This commit is contained in:
2026-04-22 17:49:33 +08:00
parent 562a9b5172
commit eced5c75d7
23 changed files with 790 additions and 244 deletions

View File

@@ -7,11 +7,11 @@
<title>邀请合作伙伴_发展代理享收益_推广返利计划_赤眉</title> <title>邀请合作伙伴_发展代理享收益_推广返利计划_赤眉</title>
<meta name="description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。"> <meta name="description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。">
<meta name="keywords" content="渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台"> <meta name="keywords" content="渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台">
<meta property="og:title" content="邀请合作伙伴_发展代理享收益_推广返利计划_赤眉"> <meta property="og:title" content="邀请合作伙伴_发展代理享收益_推广返利计划_赤眉">
<meta property="og:description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。"> <meta property="og:description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。">
<meta property="og:url" content="https://www.xxxxxx.com/agent/invitation"> <meta property="og:url" content="https://www.xxxxxx.com/agent/invitation">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:site_name" content="赤眉"> <meta property="og:site_name" content="赤眉">
@@ -21,7 +21,7 @@
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:title" content="邀请合作伙伴_发展代理享收益_推广返利计划_赤眉"> <meta name="twitter:title" content="邀请合作伙伴_发展代理享收益_推广返利计划_赤眉">
<meta name="twitter:description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。"> <meta name="twitter:description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。">
<meta name="twitter:url" content="https://www.xxxxxx.com/agent/invitation"> <meta name="twitter:url" content="https://www.xxxxxx.com/agent/invitation">
<link rel="canonical" href="https://www.xxxxxx.com/agent/invitation"> <link rel="canonical" href="https://www.xxxxxx.com/agent/invitation">
@@ -31,7 +31,7 @@
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebPage", "@type": "WebPage",
"name": "邀请合作伙伴_发展代理享收益_推广返利计划_赤眉", "name": "邀请合作伙伴_发展代理享收益_推广返利计划_赤眉",
"description": "赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。", "description": "赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。",
"url": "https://www.xxxxxx.com/agent/invitation", "url": "https://www.xxxxxx.com/agent/invitation",
"mainEntity": { "mainEntity": {
"@type": "Organization", "@type": "Organization",
@@ -61,7 +61,7 @@
<p>正在跳转到完整版网站...</p> <p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.xxxxxx.com/agent/invitation">点击这里</a></p> <p>如果浏览器没有自动跳转,请 <a href="https://www.xxxxxx.com/agent/invitation">点击这里</a></p>
</div> </div>
<p>赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。</p> <p>赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。</p>
<section> <section>
<h2>关于赤眉</h2> <h2>关于赤眉</h2>
<p>赤眉提供背调报告代理加盟与个人风控系统搭建,合规数据服务平台。支持职场背调、家政背景核验、企业风控服务与数据报告分销。</p> <p>赤眉提供背调报告代理加盟与个人风控系统搭建,合规数据服务平台。支持职场背调、家政背景核验、企业风控服务与数据报告分销。</p>

View File

@@ -127,7 +127,7 @@ function buildPageSEOConfigs() {
"agent-invitation.html": { "agent-invitation.html": {
title: `邀请合作伙伴_发展代理享收益_推广返利计划_${N}`, title: `邀请合作伙伴_发展代理享收益_推广返利计划_${N}`,
description: description:
`${N}推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。`, `${N}推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。`,
keywords: keywords:
"渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台", "渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台",
url: `${O}/agent/invitation`, url: `${O}/agent/invitation`,

View File

@@ -6,13 +6,15 @@ if (API_URL === undefined) throw new Error("缺少环境变量: VITE_API_URL");
const API_PREFIX = import.meta.env.VITE_API_PREFIX; const API_PREFIX = import.meta.env.VITE_API_PREFIX;
if (!API_PREFIX) throw new Error("缺少环境变量: VITE_API_PREFIX"); if (!API_PREFIX) throw new Error("缺少环境变量: VITE_API_PREFIX");
// 注销账号API // 注销账号API(需传入短信验证码)
export function cancelAccount() { export function cancelAccount(code) {
return axios({ return axios({
method: "post", method: "post",
url: `${API_URL}${API_PREFIX}/user/cancelOut`, url: `${API_URL}${API_PREFIX}/user/cancelOut`,
data: { code },
headers: { headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`, Authorization: `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json",
}, },
}); });
} }

View File

@@ -0,0 +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="1776519954569" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6257" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M574.08 480a323.392 323.392 0 0 1 298.816 199.68c48.96 118.208-38.016 248.32-166.016 248.32H316.608c-128 0-214.912-130.112-165.888-248.32A323.392 323.392 0 0 1 449.472 480H574.08z m35.2 126.72a32 32 0 0 0-45.248 0l-52.096 51.968-51.968-52.032a32 32 0 0 0-45.248 45.312l51.968 51.968-51.968 52.032a32 32 0 0 0 45.248 45.312l51.968-52.096 52.096 52.096a32 32 0 1 0 45.248-45.248l-52.096-52.096 52.096-52.032a32 32 0 0 0 0-45.248zM512 96a160 160 0 1 1 0 320 160 160 0 0 1 0-320z" p-id="6258" fill="#8A8A8A"></path></svg>

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 KiB

After

Width:  |  Height:  |  Size: 676 KiB

View File

@@ -0,0 +1,15 @@
/**
* App 内嵌 WebView 通过 URL 携带 `?source=app&token=`。
* 在路由守卫最前写入 localStorage保证 requiresAuth 与 useApiFetch 能读到 token。
*/
export function applyAppWebViewTokenFromRoute(to) {
try {
const q = to?.query
if (!q || q.source !== 'app' || !q.token)
return
localStorage.setItem('token', String(q.token))
}
catch {
/* ignore */
}
}

View File

@@ -73,7 +73,7 @@
</div> --> </div> -->
<!-- 免责声明 --> <!-- 免责声明 -->
<div class="text-xs text-center text-gray-500 leading-relaxed mt-2"> <div class="text-xs text-center text-gray-500 leading-relaxed mt-2">
为保证用户的隐私及数据安全查询结果生成30天后将自动删除 为保证用户的隐私及数据安全查询结果生成{{ appConfig.query.retention_days }}天后将自动删除
</div> </div>
</div> </div>
@@ -259,7 +259,7 @@
v-html="featureData.description"> v-html="featureData.description">
</div> </div>
<div class="mb-2 text-xs italic text-danger"> <div class="mb-2 text-xs italic text-danger">
为保证用户的隐私以及数据安全,查询的结果生成30天之后将自动清除。 为保证用户的隐私以及数据安全,查询的结果生成{{ appConfig.query.retention_days }}天之后将自动清除。
</div> </div>
</div> </div>
</div> </div>
@@ -285,6 +285,7 @@ import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore"; import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv"; import { useEnv } from "@/composables/useEnv";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha"; import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
import { useAppConfig } from "@/composables/useAppConfig";
import { showConfirmDialog } from "vant"; import { showConfirmDialog } from "vant";
import Payment from "@/components/Payment.vue"; import Payment from "@/components/Payment.vue";
@@ -359,6 +360,7 @@ const dialogStore = useDialogStore();
const userStore = useUserStore(); const userStore = useUserStore();
const { isWeChat } = useEnv(); const { isWeChat } = useEnv();
const { runWithCaptcha } = useAliyunCaptcha(); const { runWithCaptcha } = useAliyunCaptcha();
const { appConfig, loadAppConfig } = useAppConfig();
// 响应式数据 // 响应式数据
const showPayment = ref(false); const showPayment = ref(false);
@@ -710,6 +712,7 @@ const toHistory = () => {
onMounted(async () => { onMounted(async () => {
await loadBackgroundImage(); await loadBackgroundImage();
await loadTrapezoidBackground(); await loadTrapezoidBackground();
await loadAppConfig();
}); });
// 加载背景图片 // 加载背景图片

View File

@@ -12,19 +12,15 @@
<div class="font-bold text-xl">{{ data.product_name }}</div> <div class="font-bold text-xl">{{ data.product_name }}</div>
<div class="text-3xl text-red-500 font-bold"> <div class="text-3xl text-red-500 font-bold">
<!-- 显示原价和折扣价格 --> <!-- 显示原价和折扣价格 -->
<div <div v-if="discountPrice" class="line-through text-gray-500 mt-4" :class="{ 'text-2xl': discountPrice }">
v-if="discountPrice" ¥ {{ displayAmount }}
class="line-through text-gray-500 mt-4"
:class="{ 'text-2xl': discountPrice }"
>
¥ {{ data.sell_price }}
</div> </div>
<div> <div>
¥ ¥
{{ {{
discountPrice discountPrice
? (data.sell_price * 0.2).toFixed(2) ? displayDiscountAmount
: data.sell_price : displayAmount
}} }}
</div> </div>
</div> </div>
@@ -115,7 +111,7 @@
</template> </template>
<script setup> <script setup>
import { ref, defineProps, watch } from "vue"; import { computed, ref, defineProps, watch } from "vue";
const { isWeChat } = useEnv(); const { isWeChat } = useEnv();
const isDev = import.meta.env.DEV; const isDev = import.meta.env.DEV;
@@ -136,6 +132,30 @@ const props = defineProps({
const show = defineModel(); const show = defineModel();
const selectedPaymentMethod = ref(isWeChat.value ? "wechat" : "alipay"); const selectedPaymentMethod = ref(isWeChat.value ? "wechat" : "alipay");
function toFiniteNumber(value) {
const n = typeof value === "number" ? value : Number(value);
return Number.isFinite(n) ? n : null;
}
const payableAmount = computed(() => {
const candidates = [props?.data?.sell_price, props?.data?.price, props?.data?.amount];
for (const item of candidates) {
const normalized = toFiniteNumber(item);
if (normalized !== null) return normalized;
}
return null;
});
const displayAmount = computed(() => {
if (payableAmount.value === null) return "--";
return payableAmount.value.toFixed(2);
});
const displayDiscountAmount = computed(() => {
if (payableAmount.value === null) return "--";
return (payableAmount.value * 0.2).toFixed(2);
});
function setDefaultPaymentMethod() { function setDefaultPaymentMethod() {
selectedPaymentMethod.value = isWeChat.value ? "wechat" : "alipay"; selectedPaymentMethod.value = isWeChat.value ? "wechat" : "alipay";
} }

View File

@@ -6,6 +6,53 @@ import { useAgentStore } from "@/stores/agentStore";
const API_PREFIX = import.meta.env.VITE_API_PREFIX; const API_PREFIX = import.meta.env.VITE_API_PREFIX;
if (!API_PREFIX) throw new Error("缺少环境变量: VITE_API_PREFIX"); if (!API_PREFIX) throw new Error("缺少环境变量: VITE_API_PREFIX");
const TEMP_USER_INVALID_CODE = 100012;
let isWxh5Reauthing = false;
const isMiniProgramWebview = () => /miniprogram/.test(navigator.userAgent.toLowerCase());
const isWechatBrowser = () => /micromessenger/.test(navigator.userAgent.toLowerCase());
const clearAuthData = () => {
localStorage.removeItem("token");
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
localStorage.removeItem("userInfo");
localStorage.removeItem("agentInfo");
};
const handleWxh5TempUserInvalid = () => {
if (isWxh5Reauthing) {
return;
}
isWxh5Reauthing = true;
clearAuthData();
const userStore = useUserStore();
const agentStore = useAgentStore();
userStore.resetUser();
agentStore.resetAgent();
// webview 端只处理微信公众号 H5跳过小程序内 webview 场景
if (isMiniProgramWebview()) {
router.replace("/login");
return;
}
if (!isWechatBrowser()) {
router.replace("/login");
return;
}
const appId = import.meta.env.VITE_WECHAT_APP_ID;
if (!appId) {
router.replace("/login");
return;
}
const redirectUri = encodeURIComponent(window.location.href);
const state = "snsapi_base";
const scope = "snsapi_base";
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
window.location.href = authUrl;
};
// 创建全局的 fetch 实例 // 创建全局的 fetch 实例
const useApiFetch = createFetch({ const useApiFetch = createFetch({
baseUrl: API_PREFIX, // 你的 API 基础路径 baseUrl: API_PREFIX, // 你的 API 基础路径
@@ -25,9 +72,7 @@ const useApiFetch = createFetch({
// 在请求前添加通用的 Header例如 Authorization // 在请求前添加通用的 Header例如 Authorization
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
let platform = "h5"; let platform = "h5";
const userAgent = navigator.userAgent.toLowerCase(); if (!isMiniProgramWebview() && isWechatBrowser()) {
const isWechat = /micromessenger/.test(userAgent);
if (isWechat) {
platform = "wxh5"; platform = "wxh5";
} }
options.headers['X-Platform'] = platform options.headers['X-Platform'] = platform
@@ -53,13 +98,13 @@ const useApiFetch = createFetch({
} }
if (data.code !== 200) { if (data.code !== 200) {
if (data.code === TEMP_USER_INVALID_CODE) {
handleWxh5TempUserInvalid();
return { data, response };
}
if (data.code === 100009) { if (data.code === 100009) {
// 用户不存在:清除并刷新 // 用户不存在:清除并刷新
localStorage.removeItem('token') clearAuthData();
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
const userStore = useUserStore(); const userStore = useUserStore();
const agentStore = useAgentStore(); const agentStore = useAgentStore();
@@ -70,18 +115,23 @@ const useApiFetch = createFetch({
if (data.code === 100011) { if (data.code === 100011) {
// 账号已被封禁:提示并跳转登录 // 账号已被封禁:提示并跳转登录
showToast({ message: data.msg || "账号已被封禁" }); showToast({ message: data.msg || "账号已被封禁" });
localStorage.removeItem('token') clearAuthData();
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
const userStore = useUserStore(); const userStore = useUserStore();
const agentStore = useAgentStore(); const agentStore = useAgentStore();
userStore.resetUser() userStore.resetUser()
agentStore.resetAgent() agentStore.resetAgent()
router.replace("/login"); router.replace("/login");
} }
if (data.code !== 200002 && data.code !== 200003 && data.code !== 200004 && data.code !== 100009 && data.code !== 100011) { if (data.code === 100013) {
showToast({ message: data.msg || "账号已注销" });
clearAuthData();
const userStore = useUserStore();
const agentStore = useAgentStore();
userStore.resetUser()
agentStore.resetAgent()
router.replace("/login");
}
if (data.code !== 200002 && data.code !== 200003 && data.code !== 200004 && data.code !== 100009 && data.code !== 100011 && data.code !== 100013 && data.code !== TEMP_USER_INVALID_CODE) {
showToast({ message: data.msg }); showToast({ message: data.msg });
} }
} }
@@ -96,8 +146,8 @@ const useApiFetch = createFetch({
localStorage.removeItem('accessExpire') localStorage.removeItem('accessExpire')
router.replace("/login"); router.replace("/login");
} else if (response?.status === 403) { } else if (response?.status === 403) {
// 账号已被封禁 const msg = response?.data?.msg || (response?.data?.code === 100013 ? "账号已注销" : "账号已被封禁");
showToast({ message: "账号已被封禁" }); showToast({ message: msg });
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem('refreshAfter') localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire') localStorage.removeItem('accessExpire')

View File

@@ -0,0 +1,51 @@
import { ref } from "vue";
import useApiFetch from "@/composables/useApiFetch";
const DEFAULT_RETENTION_DAYS = 30;
const appConfig = ref({
query: {
retention_days: DEFAULT_RETENTION_DAYS,
},
});
let loadingPromise = null;
function normalizeRetentionDays(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return DEFAULT_RETENTION_DAYS;
}
return Math.floor(value);
}
export function useAppConfig() {
async function loadAppConfig(force = false) {
if (!force && loadingPromise) {
return loadingPromise;
}
loadingPromise = (async () => {
const { data, error } = await useApiFetch("/app/config").get().json();
if (error.value || data.value?.code !== 200 || !data.value?.data) {
return appConfig.value;
}
const payload = data.value.data;
appConfig.value = {
query: {
retention_days: normalizeRetentionDays(payload.query?.retention_days),
},
};
return appConfig.value;
})();
try {
return await loadingPromise;
} finally {
loadingPromise = null;
}
}
return {
appConfig,
loadAppConfig,
};
}

View File

@@ -264,7 +264,7 @@ export function useSEO() {
"/agent/invitation": { "/agent/invitation": {
title: `邀请合作伙伴_发展代理享收益_推广返利计划_${SEO_SITE_NAME}`, title: `邀请合作伙伴_发展代理享收益_推广返利计划_${SEO_SITE_NAME}`,
description: description:
"赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。", "赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。",
keywords: keywords:
"渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台", "渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台",
}, },

View File

@@ -2,9 +2,10 @@
<router-view /> <router-view />
<van-popup v-model:show="showPopup" round @click-overlay="onClickOverlay"> <van-popup v-model:show="showPopup" round @click-overlay="onClickOverlay">
<div class="popup-content text-center p-8"> <div class="popup-content text-center p-8">
<div v-if="currentNotify?.title" class="text-lg font-bold mb-4">{{ currentNotify.title }}</div>
<div v-html="currentNotify?.content"></div> <div v-html="currentNotify?.content"></div>
<div class="flex justify-center"> <div class="flex justify-center">
<van-button type="primary" @click="showPopup = false" class="w-24">关闭</van-button> <van-button type="primary" @click="onClosePopup" class="w-24">关闭</van-button>
</div> </div>
</div> </div>
</van-popup> </van-popup>
@@ -18,12 +19,16 @@ import { useRoute } from 'vue-router'
const showPopup = ref(false) const showPopup = ref(false)
const notify = ref([]) const notify = ref([])
const currentNotify = ref(null) const currentNotify = ref(null)
const pendingNotifyQueue = ref([])
const shownNotificationKeys = ref(new Set())
const SESSION_KEY = 'bdrp_webview_shown_notifications'
// 获取当前页面路径 // 获取当前页面路径
const route = useRoute() const route = useRoute()
// 获取通知数据 // 获取通知数据
onMounted(() => { onMounted(() => {
loadShownNotificationKeys()
getGlobalNotify() getGlobalNotify()
}) })
@@ -33,50 +38,113 @@ const getGlobalNotify = async () => {
.get() .get()
.json() .json()
if (data.value && !error.value) { if (data.value && !error.value && data.value.code === 200 && data.value.data) {
if (data.value !== 200) { notify.value = data.value.data.notifications ?? []
notify.value = data.value.data.notifications checkNotification() // 在获取数据后检查通知
checkNotification() // 在获取数据后检查通知
}
} }
} }
// 判断当前时间是否在通知的时间范围内 /** 与后台「展示时间」一致:未配置或起止相同视为「全天」,任意时刻都算在范围内 */
const isWithinTimeRange = (startTime, endTime) => { const isWithinTimeRange = (startTime, endTime) => {
const s = (startTime || '').trim()
const e = (endTime || '').trim()
if (!s && !e) return true
if (s === e) return true
const now = new Date() const now = new Date()
// 获取当前时间的小时和分钟
const currentMinutes = now.getHours() * 60 + now.getMinutes() const currentMinutes = now.getHours() * 60 + now.getMinutes()
const toMinutes = (t) => {
// 将 startTime 和 endTime 转换为分钟数 const parts = t.split(':').map(Number)
const startParts = startTime.split(':').map(Number) const h = parts[0] ?? 0
const endParts = endTime.split(':').map(Number) const m = parts[1] ?? 0
const startMinutes = startParts[0] * 60 + startParts[1] return h * 60 + m
const endMinutes = endParts[0] * 60 + endParts[1]
// 如果 endTime 小于 startTime表示跨越了午夜
if (endMinutes < startMinutes) {
// 判断当前时间是否在 [startTime, 23:59:59] 或 [00:00:00, endTime] 之间
return currentMinutes >= startMinutes || currentMinutes < endMinutes
} }
const startMinutes = toMinutes(s)
const endMinutes = toMinutes(e)
// 普通情况,直接判断时间是否在范围内 if (endMinutes < startMinutes) {
return currentMinutes >= startMinutes || currentMinutes <= endMinutes
}
return currentMinutes >= startMinutes && currentMinutes <= endMinutes return currentMinutes >= startMinutes && currentMinutes <= endMinutes
} }
/** 当前路由是否与通知配置的页面一致(后台填的路径需与实际 path 一致) */
const matchesNotificationPage = (page) => {
const p = (page || '').trim()
const cur = route.path || ''
if (p === cur) return true
// 首页常见:后台填 "/",路由可能是 "/" 或 ""
if (p === '/' && (cur === '/' || cur === '')) return true
return false
}
const buildNotificationKey = (notification) => {
return [
notification.title ?? '',
notification.content ?? '',
notification.notificationPage ?? '',
notification.startDate ?? '',
notification.endDate ?? '',
notification.startTime ?? '',
notification.endTime ?? ''
].join('|')
}
const loadShownNotificationKeys = () => {
const raw = sessionStorage.getItem(SESSION_KEY)
if (!raw) return
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) {
shownNotificationKeys.value = new Set(parsed)
}
} catch {
shownNotificationKeys.value = new Set()
}
}
const saveShownNotificationKeys = () => {
sessionStorage.setItem(SESSION_KEY, JSON.stringify([...shownNotificationKeys.value]))
}
const hasShownNotification = (notification) => {
return shownNotificationKeys.value.has(buildNotificationKey(notification))
}
const markNotificationShown = (notification) => {
shownNotificationKeys.value.add(buildNotificationKey(notification))
saveShownNotificationKeys()
}
const showNextNotification = () => {
const next = pendingNotifyQueue.value.shift()
if (!next) {
currentNotify.value = null
showPopup.value = false
return
}
currentNotify.value = next
showPopup.value = true
markNotificationShown(next)
}
// 检查通知并更新showPopup // 检查通知并更新showPopup
const checkNotification = () => { const checkNotification = () => {
// 遍历通知数组,找到第一个符合条件的通知 if (showPopup.value) return
for (let notification of notify.value) {
// 判断时间是否符合当前时间
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
// 判断页面是否符合
if (isTimeValid && notification.notificationPage === route.path) { const matchedNotifications = []
currentNotify.value = notification for (let notification of notify.value) {
showPopup.value = true const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
break // 只显示第一个符合的通知 const isPageValid = matchesNotificationPage(notification.notificationPage)
const isShown = hasShownNotification(notification)
if (isTimeValid && isPageValid && !isShown) {
matchedNotifications.push(notification)
} }
} }
pendingNotifyQueue.value = matchedNotifications
showNextNotification()
} }
// 监听路由变化 // 监听路由变化
@@ -85,8 +153,12 @@ watch(() => route.path, () => {
}) })
// 关闭弹窗 // 关闭弹窗
const onClosePopup = () => {
showNextNotification()
}
const onClickOverlay = () => { const onClickOverlay = () => {
showPopup.value = false onClosePopup()
} }
</script> </script>

View File

@@ -11,6 +11,7 @@ import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv"; import { useEnv } from "@/composables/useEnv";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useSEO } from "@/composables/useSEO"; import { useSEO } from "@/composables/useSEO";
import { applyAppWebViewTokenFromRoute } from "@/bootstrap/appWebviewToken";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -123,6 +124,21 @@ const router = createRouter({
import("@/views/UserAgreement.vue"), import("@/views/UserAgreement.vue"),
meta: { title: "用户协议" }, meta: { title: "用户协议" },
}, },
{
path: "/accountCancelAgreement",
name: "accountCancelAgreement",
component: () =>
import("@/views/AccountCancelAgreement.vue"),
meta: { title: "账号注销协议" },
},
{
path: "/cancelAccount",
name: "cancelAccount",
component: () =>
import("@/views/CancelAccount.vue"),
meta: { title: "注销账号", requiresAuth: true },
},
{ {
path: "/agentManageAgreement", path: "/agentManageAgreement",
name: "agentManageAgreement", name: "agentManageAgreement",
@@ -319,6 +335,42 @@ const router = createRouter({
import("@/views/AgentServiceAgreement.vue"), import("@/views/AgentServiceAgreement.vue"),
meta: { title: "信息技术服务合同" }, meta: { title: "信息技术服务合同" },
}, },
{
path: "/accountCancelAgreement",
name: "accountCancelAgreementApp",
component: () =>
import("@/views/AccountCancelAgreement.vue"),
meta: { title: "账号注销协议" },
},
{
path: "cancelAccount",
name: "appCancelAccount",
component: () =>
import("@/views/CancelAccount.vue"),
meta: { title: "注销账号", requiresAuth: true },
},
/** 无 PageLayout 顶栏,专供 App WebView与 /report、/example 共用组件 */
{
path: "report",
name: "appReport",
component: () => import("@/views/Report.vue"),
meta: {
title: "报告结果",
requiresAuth: true,
notNeedBindPhone: true,
embedForApp: true,
},
},
{
path: "example",
name: "appExample",
component: () => import("@/views/Example.vue"),
meta: {
title: "示例报告",
notNeedBindPhone: true,
embedForApp: true,
},
},
], ],
}, },
], ],
@@ -364,6 +416,7 @@ NProgress.configure({
// 路由导航守卫 // 路由导航守卫
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); // 启动进度条 NProgress.start(); // 启动进度条
applyAppWebViewTokenFromRoute(to);
const isAuthenticated = localStorage.getItem("token"); const isAuthenticated = localStorage.getItem("token");
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();

View File

@@ -0,0 +1,56 @@
<script setup>
</script>
<template>
<article class="user-agreement p-4 text-sm text-gray-800 leading-relaxed">
<h1 class="mb-4 text-center text-lg text-gray-900 font-semibold">
帐号注销协议
</h1>
<p class="mb-3 text-justify">
您在申请注销流程中点击同意前应当认真阅读帐号注销协议以下简称本协议特别提醒您当您成功提交注销申请后即表示您已充分阅读理解并接受本协议的全部内容阅读本协议的过程中如果您不同意相关任何条款请您立即停止帐号注销程序如您对本协议有任何疑问可联系我们的客服
</p>
<p class="mb-3 text-justify">
1. 如果您仍欲继续注销帐号您的帐号需同时满足以下条件
</p>
<p class="mb-2 ml-2 text-justify">
1帐号不在处罚状态中且能正常登录
</p>
<p class="mb-3 ml-2 text-justify">
2帐号最近一个月内并无修改密码修改关联手机绑定手机记录
</p>
<p class="mb-3 text-justify">
2. 您应确保您有权决定该帐号的注销事宜不侵犯任何第三方的合法权益如因此引发任何争议由您自行承担
</p>
<p class="mb-3 text-justify">
3. 您理解并同意账号注销后我们无法协助您重新恢复前述服务请您在申请注销前自行备份您欲保留的本帐号信息和数据
</p>
<p class="mb-3 text-justify">
4. 帐号注销后已绑定的手机号认证信息将会消失且无法注册
</p>
<p class="mb-3 text-justify">
5. 注销帐号后您将无法再使用本帐号也将无法找回您帐号中及与帐号相关的任何内容或信息包括但不限于
</p>
<p class="mb-2 ml-2 text-justify">
1您将无法继续使用该帐号进行登录
</p>
<p class="mb-2 ml-2 text-justify">
2您帐号的个人资料和历史信息包含昵称头像消费记录查询报告等都将无法找回
</p>
<p class="mb-3 ml-2 text-justify">
3您理解并同意注销帐号后您曾获得的充值余额贝壳币及其他虚拟财产等将视为您自愿主动放弃无法继续使用由此引起一切纠纷由您自行处理我们不承担任何责任
</p>
<p class="mb-3 text-justify">
6. 在帐号注销期间如果您的帐号被他人投诉被国家机关调查或者正处于诉讼仲裁程序中我们有权自行终止您的帐号注销程序而无需另行得到您的同意
</p>
<p class="mb-3 text-justify">
7. 请注意注销您的帐号并不代表本帐号注销前的帐号行为和相关责任得到豁免或减轻
</p>
</article>
</template>

View File

@@ -105,86 +105,17 @@
</div> </div>
</div> </div>
<!-- 活跃下级奖励 --> <!-- 团队奖励明细与下级 -->
<div class="rounded-xl shadow-lg bg-gradient-to-r from-success-50/50 to-success-100/40 p-6"> <div class="rounded-xl shadow-lg bg-gradient-to-r from-success-50/50 to-success-100/40 p-6">
<div class="flex justify-between items-center mb-4"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div class="flex items-center"> <button type="button" @click="goToRewardsDetail"
<van-icon name="friends" class="text-xl mr-2" style="color: var(--color-success);" /> class="w-full rounded-full border bg-white/90 py-3 px-4 shadow-sm flex items-center justify-center"
<span class="text-lg font-bold" style="color: var(--van-text-color);">活跃下级奖励</span> style="color: var(--van-text-color-2); border-color: var(--van-border-color);">
</div> <van-icon name="records" class="mr-1" />
<div class="text-right"> 团队奖励明细
<div class="text-2xl font-bold" style="color: var(--color-success);">
¥
{{
(data?.active_reward?.total_reward || 0).toFixed(2)
}}
</div>
<div class="text-sm mt-1" style="color: var(--van-text-color-2);">活跃下级 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
? 'text-white shadow-md'
: 'bg-white/90 border',
]" :style="selectedActiveDate === item.value
? 'background-color: var(--color-success);'
: 'color: var(--van-text-color-2); border-color: var(--van-border-color);'">
{{ item.label }}
</button> </button>
</div> <button type="button" @click="toSubordinateList"
class="w-full rounded-full py-3 px-4 shadow-md flex items-center justify-center text-white"
<div class="grid grid-cols-2 gap-2 mb-6">
<div class="p-3 rounded-lg backdrop-blur-sm" style="background-color: rgba(16, 185, 129, 0.08);">
<div class="flex items-center text-sm" style="color: var(--van-text-color-2);">
<van-icon name="medal" class="mr-1" />本日奖励
</div>
<div class="text-xl font-bold mt-1" style="color: var(--color-success);">
¥
{{ (currentActiveData.active_reward || 0).toFixed(2) }}
</div>
</div>
<div class="p-3 rounded-lg backdrop-blur-sm" style="background-color: rgba(16, 185, 129, 0.08);">
<div class="flex items-center text-sm" style="color: var(--van-text-color-2);">
<van-icon name="discount" class="mr-1" />下级推广奖励
</div>
<div class="text-xl font-bold mt-1" style="color: var(--color-success);">
¥
{{
(currentActiveData.sub_promote_reward || 0).toFixed(
2
)
}}
</div>
</div>
<div class="p-3 rounded-lg backdrop-blur-sm" style="background-color: rgba(16, 185, 129, 0.08);">
<div class="flex items-center text-sm" style="color: var(--van-text-color-2);">
<van-icon name="fire" class="mr-1" />下级转化奖励
</div>
<div class="text-xl font-bold mt-1" style="color: var(--color-success);">
¥
{{
(
currentActiveData.sub_withdraw_reward || 0
).toFixed(2)
}}
</div>
</div>
</div>
<div class="flex items-center justify-between text-sm font-semibold cursor-pointer pt-4"
style="color: var(--color-success);" @click="goToActiveDetail">
<span>查看奖励明细</span>
<span class="text-lg"></span>
</div>
<!-- 添加查看下级按钮 -->
<div class="mt-4">
<button @click="toSubordinateList"
class="w-full text-white rounded-full py-2 px-4 shadow-md flex items-center justify-center bg-success"
style="background: linear-gradient(135deg, var(--color-success), var(--color-success-600));"> style="background: linear-gradient(135deg, var(--color-success), var(--color-success-600));">
<van-icon name="friends" class="mr-1" /> <van-icon name="friends" class="mr-1" />
查看我的下级 查看我的下级
@@ -219,33 +150,12 @@ const promoteDateOptions = [
]; ];
const selectedPromoteDate = ref("today"); 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 currentPromoteData = computed(() => {
const range = dateRangeMap[selectedPromoteDate.value]; const range = dateRangeMap[selectedPromoteDate.value];
return data.value?.direct_push?.[range] || { commission: 0, report: 0 }; 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 getData = async () => {
const { data: res, error } = await useApiFetch("/agent/revenue") const { data: res, error } = await useApiFetch("/agent/revenue")
.get() .get()
@@ -264,7 +174,7 @@ onMounted(() => {
// 路由跳转 // 路由跳转
const goToPromoteDetail = () => router.push({ name: "promoteDetails" }); const goToPromoteDetail = () => router.push({ name: "promoteDetails" });
const goToActiveDetail = () => router.push({ name: "rewardsDetails" }); const goToRewardsDetail = () => router.push({ name: "rewardsDetails" });
const toWithdraw = () => router.push({ name: "withdraw" }); const toWithdraw = () => router.push({ name: "withdraw" });

View File

@@ -88,18 +88,9 @@ const getData = async () => {
if (res.value?.code === 200 && !error.value) { if (res.value?.code === 200 && !error.value) {
if (page.value === 1) { if (page.value === 1) {
data.value = res.value.data data.value = res.value.data
// 策略A前端下线“新增活跃/月度活跃奖励”
if (Array.isArray(data.value.list)) {
data.value.list = data.value.list.filter(
i => i.type !== 'descendant_new_active' && i.type !== 'descendant_stay_active'
)
}
} else { } else {
if (Array.isArray(res.value.data.list)) { if (Array.isArray(res.value.data.list)) {
const list = res.value.data.list.filter( data.value.list.push(...res.value.data.list)
i => i.type !== 'descendant_new_active' && i.type !== 'descendant_stay_active'
)
data.value.list.push(...list)
} }
} }

View File

@@ -642,6 +642,13 @@ const buttonClass = computed(() => {
// VIP价格配置 // VIP价格配置
const vipConfig = reactive({}) const vipConfig = reactive({})
function toFiniteNumber(value, fallback = 0) {
if (value === null || value === undefined || value === '')
return fallback
const n = typeof value === 'number' ? value : Number(value)
return Number.isFinite(n) ? n : fallback
}
// 计算得出的收益数据 // 计算得出的收益数据
const revenueData = computed(() => { const revenueData = computed(() => {
const baseOrders = 300 // 基础订单数 const baseOrders = 300 // 基础订单数
@@ -741,20 +748,17 @@ onMounted(async () => {
const selectedType = ref('vip') // 默认选择VIP const selectedType = ref('vip') // 默认选择VIP
const showPayment = ref(false) const showPayment = ref(false)
const payData = ref({
product_name: `${selectedType.value.toUpperCase()}代理`,
sell_price: vipConfig.price,
})
const payID = ref('') const payID = ref('')
const payData = computed(() => ({
product_name: `${selectedType.value === 'vip' ? 'VIP' : 'SVIP'}代理`,
sell_price: selectedType.value === 'vip'
? toFiniteNumber(vipConfig.price)
: toFiniteNumber(vipConfig.svipPrice),
}))
// 选择代理类型 // 选择代理类型
function selectType(type) { function selectType(type) {
selectedType.value = type selectedType.value = type
// 更新payData中的价格和产品名称
payData.value = {
product_name: `${type === 'vip' ? 'VIP' : 'SVIP'}代理`,
sell_price: type === 'vip' ? vipConfig.price : vipConfig.svipPrice,
}
} }
// 申请VIP或SVIP // 申请VIP或SVIP
@@ -771,6 +775,11 @@ async function applyVip() {
return return
} }
if (toFiniteNumber(payData.value.sell_price) <= 0) {
showToast('会员价格加载中,请稍后重试')
return
}
const { data, error } = await useApiFetch('/agent/membership/activate') const { data, error } = await useApiFetch('/agent/membership/activate')
.post({ .post({
type: selectedType.value.toUpperCase(), type: selectedType.value.toUpperCase(),

View File

@@ -176,23 +176,14 @@ import { ref, reactive, computed, onMounted } from "vue";
import { showToast } from "vant"; import { showToast } from "vant";
import { settings } from "nprogress"; import { settings } from "nprogress";
// 报告类型选项 // 报告类型选项:完全由后端返回
const reportOptions = [ const reportOptions = ref([]);
{ text: "小微企业", value: "companyinfo", id: 2 },
{ text: "贷前风险", value: "preloanbackgroundcheck", id: 5 },
{ text: "个人大数据", value: "personaldata", id: 27 },
{ text: "入职风险", value: "backgroundcheck", id: 1 },
{ text: "家政风险", value: "homeservice", id: 3 },
{ text: "婚恋风险", value: "marriage", id: 4 },
// { text: "租赁风险", value: "rentalrisk", id: 6 },
// { text: "个人风险", value: "riskassessment", id: 7 },
];
// 状态管理 // 状态管理
const showPicker = ref(false); const showPicker = ref(false);
const selectedReport = ref(reportOptions[0]); const selectedReport = ref(null);
const selectedReportText = ref(reportOptions[0].text); const selectedReportText = ref("");
const selectedReportId = ref(reportOptions[0].id); const selectedReportId = ref(null);
const configData = ref({}); const configData = ref({});
const productConfigData = ref({}); const productConfigData = ref({});
@@ -347,6 +338,7 @@ const validateRatio = () => {
// 获取配置 // 获取配置
const getConfig = async () => { const getConfig = async () => {
if (!selectedReportId.value) return;
try { try {
const { data, error } = await useApiFetch( const { data, error } = await useApiFetch(
"/agent/membership/user_config?product_id=" + selectedReportId.value "/agent/membership/user_config?product_id=" + selectedReportId.value
@@ -474,9 +466,11 @@ const finalValidation = () => {
// 选择器确认 // 选择器确认
const onConfirm = ({ selectedOptions }) => { const onConfirm = ({ selectedOptions }) => {
selectedReport.value = selectedOptions[0]; const picked = selectedOptions?.[0];
selectedReportText.value = selectedOptions[0].text; if (!picked) return;
selectedReportId.value = selectedOptions[0].id; selectedReport.value = picked;
selectedReportText.value = picked.text;
selectedReportId.value = picked.value;
showPicker.value = false; showPicker.value = false;
// 重置错误状态 // 重置错误状态
rangeError.value = false; rangeError.value = false;
@@ -490,8 +484,32 @@ const closeRangeError = () => {
rangeError.value = false; rangeError.value = false;
}, 2000); }, 2000);
}; };
onMounted(() => {
getConfig(); const loadReportOptions = async () => {
try {
const { data, error } = await useApiFetch("/agent/product_config").get().json();
if (error.value || data.value?.code !== 200) return;
const list = data.value?.data?.agent_product_config || data.value?.data?.AgentProductConfig || [];
reportOptions.value = list
.map((p) => ({
text: p.product_name || `报告${p.product_id}`,
value: p.product_id,
}))
.filter((p) => p.value != null);
if (reportOptions.value.length > 0) {
const first = reportOptions.value[0];
selectedReport.value = first;
selectedReportText.value = first.text;
selectedReportId.value = first.value;
}
} catch (e) { }
};
onMounted(async () => {
await loadReportOptions();
if (selectedReportId.value) {
getConfig();
}
}); });
</script> </script>

278
src/views/CancelAccount.vue Normal file
View File

@@ -0,0 +1,278 @@
<script setup>
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { showConfirmDialog, showToast } from 'vant'
import AccountCancelAgreement from '@/views/AccountCancelAgreement.vue'
import useApiFetch from '@/composables/useApiFetch'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
const router = useRouter()
const userStore = useUserStore()
const agentStore = useAgentStore()
const { mobile } = storeToRefs(userStore)
const { isAgent, level } = storeToRefs(agentStore)
const loading = ref(true)
const revenueData = ref(null)
const showSmsModal = ref(false)
const cancelAccountCode = ref('')
const cancelSmsCountdown = ref(0)
let cancelSmsTimer = null
function maskName(name) {
if (!name || name.length < 11)
return name
return `${name.substring(0, 3)}****${name.substring(7)}`
}
const hasWalletBalance = computed(() => {
const b = Number(revenueData.value?.balance ?? 0)
const f = Number(revenueData.value?.frozen_balance ?? 0)
return b > 0 || f > 0
})
const showBalanceWarning = computed(() => isAgent.value && hasWalletBalance.value)
const showVipLevelReminder = computed(() =>
isAgent.value && (level.value === 'VIP' || level.value === 'SVIP'),
)
const vipLevelLabel = computed(() => (level.value === 'SVIP' ? 'SVIP' : 'VIP'))
const showAnyReminder = computed(() => showBalanceWarning.value || showVipLevelReminder.value)
onMounted(async () => {
await userStore.fetchUserInfo()
if (!userStore.mobile) {
loading.value = false
showToast('请先绑定手机号')
setTimeout(() => router.back(), 1500)
return
}
try {
await agentStore.fetchAgentStatus()
if (agentStore.isAgent) {
const { data, error } = await useApiFetch('/agent/revenue').get().json()
if (!error.value && data.value?.code === 200)
revenueData.value = data.value.data
}
}
catch {
/* ignore */
}
finally {
loading.value = false
}
})
function onExit() {
router.back()
}
function startCancelSmsCountdown() {
cancelSmsCountdown.value = 60
if (cancelSmsTimer)
clearInterval(cancelSmsTimer)
cancelSmsTimer = setInterval(() => {
if (cancelSmsCountdown.value > 0)
cancelSmsCountdown.value--
else
clearInterval(cancelSmsTimer)
}, 1000)
}
onUnmounted(() => {
if (cancelSmsTimer)
clearInterval(cancelSmsTimer)
})
function openSmsModal() {
cancelAccountCode.value = ''
showSmsModal.value = true
}
function onConfirmTap() {
if (showBalanceWarning.value) {
showConfirmDialog({
title: '确认注销',
message: '您的代理账户仍有余额或待结账金额,注销后将无法通过本账号提现,确定继续注销?',
confirmButtonText: '继续',
cancelButtonText: '取消',
})
.then(() => openSmsModal())
.catch(() => {})
}
else {
openSmsModal()
}
}
function closeSmsModal() {
showSmsModal.value = false
cancelAccountCode.value = ''
}
async function sendCancelAccountSms() {
if (!mobile.value) {
showToast('请先绑定手机号')
return
}
if (cancelSmsCountdown.value > 0)
return
const { data, error } = await useApiFetch('/auth/sendSms')
.post({ mobile: mobile.value, actionType: 'cancelAccount', captchaVerifyParam: '' })
.json()
if (!error.value && data.value?.code === 200) {
showToast({ message: '验证码已发送' })
startCancelSmsCountdown()
}
else {
showToast({ message: data.value?.msg || '发送失败' })
}
}
function clearAuthAndGoHome() {
localStorage.removeItem('token')
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
userStore.resetUser()
agentStore.resetAgent()
window.location.href = '/'
}
async function submitCancelAccount() {
if (cancelAccountCode.value.length !== 6) {
showToast('请输入6位验证码')
return
}
const { data, error } = await useApiFetch('/user/cancelOut')
.post({ code: cancelAccountCode.value })
.json()
if (!error.value && data.value?.code === 200) {
showToast({ message: '账号已注销' })
closeSmsModal()
clearAuthAndGoHome()
}
else {
showToast({ message: data.value?.msg || '注销失败' })
}
}
</script>
<template>
<!-- 减导航栏占位高度避免整页超出视口 PageLayout van-nav-bar placeholder 配合 -->
<div
class="box-border flex flex-col bg-gray-50"
style="min-height: calc(100vh - var(--van-nav-bar-height, 46px)); height: calc(100vh - var(--van-nav-bar-height, 46px));"
>
<div v-if="loading" class="flex flex-1 items-center justify-center py-20 text-gray-500">
加载中...
</div>
<template v-else>
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
<div class="min-h-0 flex-1 overflow-y-auto border-b border-gray-100 bg-white">
<AccountCancelAgreement />
</div>
<div
v-if="showAnyReminder"
class="flex-shrink-0 space-y-2 border-t border-gray-100 bg-gray-50 px-4 py-3"
>
<div
v-if="showBalanceWarning"
class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900"
>
<p class="font-medium">
钱包提示
</p>
<p class="mt-1 leading-relaxed">
检测到您为代理且账户仍有余额¥{{ (revenueData?.balance ?? 0).toFixed(2) }}或待结账金额¥{{ (revenueData?.frozen_balance ?? 0).toFixed(2) }}注销后将无法通过本账号提现请确认已了解风险
</p>
</div>
<div
v-if="showVipLevelReminder"
class="rounded-lg border border-violet-200 bg-violet-50 p-3 text-sm text-violet-900"
>
<p class="font-medium">
会员提示
</p>
<p class="mt-1 leading-relaxed">
您当前为 {{ vipLevelLabel }} 代理会员注销后该账号下的代理身份与相关权益将按平台规则终止请确认已了解风险
</p>
</div>
</div>
</div>
<div class="flex-shrink-0 border-t border-gray-200 bg-gray-50 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
<div class="flex gap-3">
<button
type="button"
class="flex-1 rounded-lg border border-gray-300 bg-white py-3 text-gray-800"
@click="onExit"
>
退出
</button>
<button
type="button"
class="flex-1 rounded-lg bg-red-500 py-3 text-white"
@click="onConfirmTap"
>
确认
</button>
</div>
</div>
</template>
<div
v-if="showSmsModal"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="closeSmsModal"
>
<div class="w-full max-w-sm rounded-xl bg-white p-5 shadow-lg" @click.stop>
<p class="mb-2 text-base font-medium text-gray-800">
验证手机号
</p>
<p class="mb-4 text-sm text-gray-500">
将向 {{ maskName(mobile) }} 发送验证码
</p>
<input
v-model="cancelAccountCode"
type="text"
maxlength="6"
inputmode="numeric"
placeholder="请输入6位验证码"
class="mb-4 w-full rounded-lg border border-gray-200 px-3 py-2 text-base"
>
<div class="mb-4">
<button
type="button"
class="w-full rounded-lg bg-gray-100 py-2 text-sm text-gray-700 disabled:opacity-50"
:disabled="cancelSmsCountdown > 0"
@click="sendCancelAccountSms"
>
{{ cancelSmsCountdown > 0 ? `${cancelSmsCountdown}s` : '获取验证码' }}
</button>
</div>
<div class="flex gap-2">
<button
type="button"
class="flex-1 rounded-lg bg-gray-200 py-2 text-gray-800"
@click="closeSmsModal"
>
取消
</button>
<button
type="button"
class="flex-1 rounded-lg bg-red-500 py-2 text-white"
@click="submitCancelAccount"
>
确认注销
</button>
</div>
</div>
</div>
</div>
</template>

View File

@@ -124,6 +124,15 @@
</div> </div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" /> <img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </button>
<button v-if="isLoggedIn && mobile"
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 hover:bg-red-50 transition-colors"
@click="goCancelAccountPage">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/cancelAccount.svg" class="w-6 h-6" alt="注销账号" />
<span class="text-gray-700 font-medium">注销账号</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
<button v-if="isLoggedIn && !isWeChat" <button v-if="isLoggedIn && !isWeChat"
class="w-full flex items-center justify-between px-6 py-4 hover:bg-red-50 transition-colors" class="w-full flex items-center justify-between px-6 py-4 hover:bg-red-50 transition-colors"
@click="handleLogout"> @click="handleLogout">
@@ -142,13 +151,13 @@
<script setup> <script setup>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { ref, computed, onBeforeMount } from "vue"; import { ref, computed, onBeforeMount } from "vue";
import { showToast } from "vant";
const router = useRouter(); const router = useRouter();
import headShot from "@/assets/images/head_shot.webp"; import headShot from "@/assets/images/head_shot.webp";
import { useAgentStore } from "@/stores/agentStore"; import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore"; import { useUserStore } from "@/stores/userStore";
import { useEnv } from "@/composables/useEnv"; import { useEnv } from "@/composables/useEnv";
import { useDialogStore } from "@/stores/dialogStore"; import { useDialogStore } from "@/stores/dialogStore";
import useApiFetch from "@/composables/useApiFetch";
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();
const dialogStore = useDialogStore(); const dialogStore = useDialogStore();
@@ -235,22 +244,12 @@ function handleLogout() {
location.reload(); location.reload();
} }
function handleCancelAccount() { function goCancelAccountPage() {
// 账号注销功能 if (!mobile.value) {
if (confirm("注销账号后,您的所有数据将被清除且无法恢复,确定要继续吗?")) { showToast("请先绑定手机号");
useApiFetch("/user/cancelOut") return;
.post()
.json()
.then(({ data, error }) => {
if (!error && data.value && data.value.code === 200) {
alert("账号已注销");
handleLogout();
}
})
.catch((error) => {
console.error("注销账号失败:", error);
});
} }
router.push({ name: "cancelAccount" });
} }
function toService() { function toService() {

View File

@@ -35,9 +35,9 @@
:product-config="pickerProductConfig" @change="onPriceChange" /> :product-config="pickerProductConfig" @change="onPriceChange" />
<div class="flex items-center justify-between my-2"> <div class="flex items-center justify-between my-2">
<div class="text-sm text-gray-500">推广收益为 <span class="text-orange-500">{{ promotionRevenue <div class="text-sm text-gray-500">推广收益为 <span class="text-orange-500">{{ promotionRevenue
}}</span> </div> }}</span> </div>
<div class="text-sm text-gray-500">我的成本为 <span class="text-orange-500">{{ costPrice <div class="text-sm text-gray-500">我的成本为 <span class="text-orange-500">{{ costPrice
}}</span> </div> }}</span> </div>
</div> </div>
</van-cell-group> </van-cell-group>
</div> </div>
@@ -87,16 +87,6 @@
<script setup> <script setup>
import PriceInputPopup from '@/components/PriceInputPopup.vue'; import PriceInputPopup from '@/components/PriceInputPopup.vue';
import VipBanner from '@/components/VipBanner.vue'; import VipBanner from '@/components/VipBanner.vue';
const reportTypes = [
{ text: "小微企业", value: "companyinfo", id: 2 },
{ text: "贷前风险", value: "preloanbackgroundcheck", id: 5 },
{ text: "个人大数据", value: "personaldata", id: 27 },
{ text: '入职风险', value: 'backgroundcheck', id: 1 },
{ text: '家政风险', value: 'homeservice', id: 3 },
{ text: '婚恋风险', value: 'marriage', id: 4 },
{ text: '租赁风险', value: 'rentalrisk', id: 6 },
];
const showTypePicker = ref(false); const showTypePicker = ref(false);
const showApplyPopup = ref(false); // 用来控制申请代理弹窗的显示 const showApplyPopup = ref(false); // 用来控制申请代理弹窗的显示
const showPricePicker = ref(false); const showPricePicker = ref(false);
@@ -107,6 +97,16 @@ const selectedReportType = ref([]);
const clientPrice = ref(null); const clientPrice = ref(null);
const productConfig = ref(null); const productConfig = ref(null);
const linkIdentifier = ref("") const linkIdentifier = ref("")
const reportTypes = computed(() => {
if (!productConfig.value?.length) return []
return productConfig.value
.map(item => ({
id: item.product_id,
text: item.product_name || `产品${item.product_id}`,
value: item.product_en || '',
}))
.filter(item => !!item.value)
})
@@ -185,10 +185,10 @@ onMounted(() => {
// getAgentInfo(); // getAgentInfo();
}); });
const SelectTypePicker = (reportType) => { const SelectTypePicker = (reportType) => {
selectedReportType.value = [reportType]; selectedReportType.value = [reportType.value];
pickerFieldText.value = reportType.text; pickerFieldText.value = reportType.text;
pickerFieldVal.value = reportType.value; pickerFieldVal.value = reportType.value;
for (let i of productConfig.value) { for (let i of (productConfig.value || [])) {
if (i.product_id === reportType.id) { if (i.product_id === reportType.id) {
pickerProductConfig.value = i pickerProductConfig.value = i
clientPrice.value = i.cost_price clientPrice.value = i.cost_price
@@ -202,8 +202,16 @@ const getPromoteConfig = async () => {
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
productConfig.value = data.value.data.AgentProductConfig; productConfig.value = data.value.data.AgentProductConfig || [];
SelectTypePicker(reportTypes[0]) if (reportTypes.value.length > 0) {
SelectTypePicker(reportTypes.value[0])
} else {
pickerProductConfig.value = null
pickerFieldText.value = ''
pickerFieldVal.value = null
clientPrice.value = null
showToast({ message: '暂无可推广产品' });
}
} else { } else {
console.log("Error fetching agent info", data.value); console.log("Error fetching agent info", data.value);
} }

View File

@@ -119,9 +119,7 @@ const fetchRewardDetails = async () => {
} }
// 处理列表数据 // 处理列表数据
if (data.value.data.list) { if (data.value.data.list) {
const list = data.value.data.list.filter( const list = data.value.data.list
i => i.type !== 'descendant_new_active' && i.type !== 'descendant_stay_active',
)
if (page.value === 1) { if (page.value === 1) {
rewardDetails.value = list rewardDetails.value = list
} else { } else {

View File

@@ -16,6 +16,12 @@ import rentalinfo from "@/assets/images/index/rentalinfo_bg.png";
import bannerImg from "@/assets/images/index/banner_1.png"; import bannerImg from "@/assets/images/index/banner_1.png";
import bannerImg2 from "@/assets/images/index/banner_2.png"; import bannerImg2 from "@/assets/images/index/banner_2.png";
import bannerImg3 from "@/assets/images/index/banner_3.png"; import bannerImg3 from "@/assets/images/index/banner_3.png";
import indexPromoteIcon from "@/assets/images/index/tgbg.png";
import indexInvitationIcon from "@/assets/images/index/yqhy.png";
import indexMyReportIcon from "@/assets/images/index/wdbg.png";
import { useAppConfig } from "@/composables/useAppConfig";
const { appConfig, loadAppConfig } = useAppConfig();
void loadAppConfig();
function toInquire(name) { function toInquire(name) {
router.push(`/inquire/${name}`); router.push(`/inquire/${name}`);
} }
@@ -115,7 +121,9 @@ const banners = ref([
<div class="text-center flex flex-col justify-center items-center" @click="toPromote"> <div class="text-center flex flex-col justify-center items-center" @click="toPromote">
<div <div
class="h-16 w-16 p-1 box-content bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center"> class="h-16 w-16 p-1 box-content bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/index/tgbg.png" alt="推广报告" class="h-12 w-12" /> <div class="h-12 w-12 shrink-0 bg-contain bg-center bg-no-repeat select-none"
role="img" aria-label="推广报告"
:style="{ backgroundImage: `url(${indexPromoteIcon})` }" />
</div> </div>
<div class="text-center mt-1 font-bold ">推广报告</div> <div class="text-center mt-1 font-bold ">推广报告</div>
</div> </div>
@@ -123,7 +131,9 @@ const banners = ref([
<div class="text-center flex flex-col justify-center items-center" @click="toInvitation"> <div class="text-center flex flex-col justify-center items-center" @click="toInvitation">
<div <div
class="h-16 w-16 p-1 box-content bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center"> class="h-16 w-16 p-1 box-content bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/index/yqhy.png" alt="邀请下级" class="h-12 w-12" /> <div class="h-12 w-12 shrink-0 bg-contain bg-center bg-no-repeat select-none"
role="img" aria-label="邀请下级"
:style="{ backgroundImage: `url(${indexInvitationIcon})` }" />
</div> </div>
<div class="text-center mt-1 font-bold ">邀请下级</div> <div class="text-center mt-1 font-bold ">邀请下级</div>
</div> </div>
@@ -137,7 +147,9 @@ const banners = ref([
<div class="text-center flex flex-col justify-center items-center" @click="toHistory"> <div class="text-center flex flex-col justify-center items-center" @click="toHistory">
<div <div
class="h-16 w-16 p-1 box-content bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center"> class="h-16 w-16 p-1 box-content bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/index/wdbg.png" alt="我的报告" class="h-12 w-12" /> <div class="h-12 w-12 shrink-0 bg-contain bg-center bg-no-repeat select-none"
role="img" aria-label="我的报告"
:style="{ backgroundImage: `url(${indexMyReportIcon})` }" />
</div> </div>
<div class="text-center mt-1 font-bold ">我的报告</div> <div class="text-center mt-1 font-bold ">我的报告</div>
</div> </div>
@@ -195,7 +207,7 @@ const banners = ref([
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="text-gray-800">我的历史查询记录</div> <div class="text-gray-800">我的历史查询记录</div>
<div class="text-xs text-gray-500">查询记录有效期为30</div> <div class="text-xs text-gray-500">查询记录有效期为{{ appConfig.query.retention_days }}</div>
</div> </div>
<img src="@/assets/images/index/right.png" alt="右箭头" class="h-6 w-6" /> <img src="@/assets/images/index/right.png" alt="右箭头" class="h-6 w-6" />
</div> </div>