f
This commit is contained in:
@@ -7,11 +7,11 @@
|
||||
|
||||
<title>邀请合作伙伴_发展代理享收益_推广返利计划_赤眉</title>
|
||||
|
||||
<meta name="description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。">
|
||||
<meta name="description" content="赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。">
|
||||
<meta name="keywords" 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:type" content="website">
|
||||
<meta property="og:site_name" content="赤眉">
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<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">
|
||||
|
||||
<link rel="canonical" href="https://www.xxxxxx.com/agent/invitation">
|
||||
@@ -31,7 +31,7 @@
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "邀请合作伙伴_发展代理享收益_推广返利计划_赤眉",
|
||||
"description": "赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。",
|
||||
"description": "赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。",
|
||||
"url": "https://www.xxxxxx.com/agent/invitation",
|
||||
"mainEntity": {
|
||||
"@type": "Organization",
|
||||
@@ -61,7 +61,7 @@
|
||||
<p>正在跳转到完整版网站...</p>
|
||||
<p>如果浏览器没有自动跳转,请 <a href="https://www.xxxxxx.com/agent/invitation">点击这里</a></p>
|
||||
</div>
|
||||
<p>赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。</p>
|
||||
<p>赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。</p>
|
||||
<section>
|
||||
<h2>关于赤眉</h2>
|
||||
<p>赤眉提供背调报告代理加盟与个人风控系统搭建,合规数据服务平台。支持职场背调、家政背景核验、企业风控服务与数据报告分销。</p>
|
||||
|
||||
@@ -127,7 +127,7 @@ function buildPageSEOConfigs() {
|
||||
"agent-invitation.html": {
|
||||
title: `邀请合作伙伴_发展代理享收益_推广返利计划_${N}`,
|
||||
description:
|
||||
`${N}推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。`,
|
||||
`${N}推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。`,
|
||||
keywords:
|
||||
"渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台",
|
||||
url: `${O}/agent/invitation`,
|
||||
|
||||
@@ -6,13 +6,15 @@ if (API_URL === undefined) throw new Error("缺少环境变量: VITE_API_URL");
|
||||
const API_PREFIX = import.meta.env.VITE_API_PREFIX;
|
||||
if (!API_PREFIX) throw new Error("缺少环境变量: VITE_API_PREFIX");
|
||||
|
||||
// 注销账号API
|
||||
export function cancelAccount() {
|
||||
// 注销账号API(需传入短信验证码)
|
||||
export function cancelAccount(code) {
|
||||
return axios({
|
||||
method: "post",
|
||||
url: `${API_URL}${API_PREFIX}/user/cancelOut`,
|
||||
data: { code },
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
1
src/assets/images/me/cancelAccount.svg
Normal file
1
src/assets/images/me/cancelAccount.svg
Normal 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 |
15
src/bootstrap/appWebviewToken.js
Normal file
15
src/bootstrap/appWebviewToken.js
Normal 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 */
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
</div> -->
|
||||
<!-- 免责声明 -->
|
||||
<div class="text-xs text-center text-gray-500 leading-relaxed mt-2">
|
||||
为保证用户的隐私及数据安全,查询结果生成30天后将自动删除
|
||||
为保证用户的隐私及数据安全,查询结果生成{{ appConfig.query.retention_days }}天后将自动删除
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
v-html="featureData.description">
|
||||
</div>
|
||||
<div class="mb-2 text-xs italic text-danger">
|
||||
为保证用户的隐私以及数据安全,查询的结果生成30天之后将自动清除。
|
||||
为保证用户的隐私以及数据安全,查询的结果生成{{ appConfig.query.retention_days }}天之后将自动清除。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -285,6 +285,7 @@ import { useUserStore } from "@/stores/userStore";
|
||||
import { useDialogStore } from "@/stores/dialogStore";
|
||||
import { useEnv } from "@/composables/useEnv";
|
||||
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
|
||||
import { useAppConfig } from "@/composables/useAppConfig";
|
||||
import { showConfirmDialog } from "vant";
|
||||
|
||||
import Payment from "@/components/Payment.vue";
|
||||
@@ -359,6 +360,7 @@ const dialogStore = useDialogStore();
|
||||
const userStore = useUserStore();
|
||||
const { isWeChat } = useEnv();
|
||||
const { runWithCaptcha } = useAliyunCaptcha();
|
||||
const { appConfig, loadAppConfig } = useAppConfig();
|
||||
|
||||
// 响应式数据
|
||||
const showPayment = ref(false);
|
||||
@@ -710,6 +712,7 @@ const toHistory = () => {
|
||||
onMounted(async () => {
|
||||
await loadBackgroundImage();
|
||||
await loadTrapezoidBackground();
|
||||
await loadAppConfig();
|
||||
});
|
||||
|
||||
// 加载背景图片
|
||||
|
||||
@@ -12,19 +12,15 @@
|
||||
<div class="font-bold text-xl">{{ data.product_name }}</div>
|
||||
<div class="text-3xl text-red-500 font-bold">
|
||||
<!-- 显示原价和折扣价格 -->
|
||||
<div
|
||||
v-if="discountPrice"
|
||||
class="line-through text-gray-500 mt-4"
|
||||
:class="{ 'text-2xl': discountPrice }"
|
||||
>
|
||||
¥ {{ data.sell_price }}
|
||||
<div v-if="discountPrice" class="line-through text-gray-500 mt-4" :class="{ 'text-2xl': discountPrice }">
|
||||
¥ {{ displayAmount }}
|
||||
</div>
|
||||
<div>
|
||||
¥
|
||||
{{
|
||||
discountPrice
|
||||
? (data.sell_price * 0.2).toFixed(2)
|
||||
: data.sell_price
|
||||
? displayDiscountAmount
|
||||
: displayAmount
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +111,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, watch } from "vue";
|
||||
import { computed, ref, defineProps, watch } from "vue";
|
||||
const { isWeChat } = useEnv();
|
||||
const isDev = import.meta.env.DEV;
|
||||
|
||||
@@ -136,6 +132,30 @@ const props = defineProps({
|
||||
const show = defineModel();
|
||||
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() {
|
||||
selectedPaymentMethod.value = isWeChat.value ? "wechat" : "alipay";
|
||||
}
|
||||
|
||||
@@ -6,6 +6,53 @@ import { useAgentStore } from "@/stores/agentStore";
|
||||
|
||||
const API_PREFIX = import.meta.env.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 实例
|
||||
const useApiFetch = createFetch({
|
||||
baseUrl: API_PREFIX, // 你的 API 基础路径
|
||||
@@ -25,9 +72,7 @@ const useApiFetch = createFetch({
|
||||
// 在请求前添加通用的 Header,例如 Authorization
|
||||
const token = localStorage.getItem("token");
|
||||
let platform = "h5";
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const isWechat = /micromessenger/.test(userAgent);
|
||||
if (isWechat) {
|
||||
if (!isMiniProgramWebview() && isWechatBrowser()) {
|
||||
platform = "wxh5";
|
||||
}
|
||||
options.headers['X-Platform'] = platform
|
||||
@@ -53,13 +98,13 @@ const useApiFetch = createFetch({
|
||||
}
|
||||
|
||||
if (data.code !== 200) {
|
||||
if (data.code === TEMP_USER_INVALID_CODE) {
|
||||
handleWxh5TempUserInvalid();
|
||||
return { data, response };
|
||||
}
|
||||
if (data.code === 100009) {
|
||||
// 用户不存在:清除并刷新
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('refreshAfter')
|
||||
localStorage.removeItem('accessExpire')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('agentInfo')
|
||||
clearAuthData();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const agentStore = useAgentStore();
|
||||
@@ -70,18 +115,23 @@ const useApiFetch = createFetch({
|
||||
if (data.code === 100011) {
|
||||
// 账号已被封禁:提示并跳转登录
|
||||
showToast({ message: data.msg || "账号已被封禁" });
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('refreshAfter')
|
||||
localStorage.removeItem('accessExpire')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('agentInfo')
|
||||
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) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -96,8 +146,8 @@ const useApiFetch = createFetch({
|
||||
localStorage.removeItem('accessExpire')
|
||||
router.replace("/login");
|
||||
} else if (response?.status === 403) {
|
||||
// 账号已被封禁
|
||||
showToast({ message: "账号已被封禁" });
|
||||
const msg = response?.data?.msg || (response?.data?.code === 100013 ? "账号已注销" : "账号已被封禁");
|
||||
showToast({ message: msg });
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem('refreshAfter')
|
||||
localStorage.removeItem('accessExpire')
|
||||
|
||||
51
src/composables/useAppConfig.js
Normal file
51
src/composables/useAppConfig.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -264,7 +264,7 @@ export function useSEO() {
|
||||
"/agent/invitation": {
|
||||
title: `邀请合作伙伴_发展代理享收益_推广返利计划_${SEO_SITE_NAME}`,
|
||||
description:
|
||||
"赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单不仅可获得推广收益,还可叠加合作伙伴活跃奖励及定价差额收益。打造专属推广团队,实现收益持续增长。",
|
||||
"赤眉推出合伙人邀请奖励机制。邀请好友注册成为合作伙伴,每单可获得推广收益及定价差额收益。打造专属推广团队,实现收益持续增长。",
|
||||
keywords:
|
||||
"渠道合作伙伴,商业收益管理,流量合规变现,业务推广系统,代理后台",
|
||||
},
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<router-view />
|
||||
<van-popup v-model:show="showPopup" round @click-overlay="onClickOverlay">
|
||||
<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 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>
|
||||
</van-popup>
|
||||
@@ -18,12 +19,16 @@ import { useRoute } from 'vue-router'
|
||||
const showPopup = ref(false)
|
||||
const notify = ref([])
|
||||
const currentNotify = ref(null)
|
||||
const pendingNotifyQueue = ref([])
|
||||
const shownNotificationKeys = ref(new Set())
|
||||
const SESSION_KEY = 'bdrp_webview_shown_notifications'
|
||||
|
||||
// 获取当前页面路径
|
||||
const route = useRoute()
|
||||
|
||||
// 获取通知数据
|
||||
onMounted(() => {
|
||||
loadShownNotificationKeys()
|
||||
getGlobalNotify()
|
||||
})
|
||||
|
||||
@@ -33,50 +38,113 @@ const getGlobalNotify = async () => {
|
||||
.get()
|
||||
.json()
|
||||
|
||||
if (data.value && !error.value) {
|
||||
if (data.value !== 200) {
|
||||
notify.value = data.value.data.notifications
|
||||
checkNotification() // 在获取数据后检查通知
|
||||
}
|
||||
if (data.value && !error.value && data.value.code === 200 && data.value.data) {
|
||||
notify.value = data.value.data.notifications ?? []
|
||||
checkNotification() // 在获取数据后检查通知
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前时间是否在通知的时间范围内
|
||||
/** 与后台「展示时间」一致:未配置或起止相同视为「全天」,任意时刻都算在范围内 */
|
||||
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 currentMinutes = now.getHours() * 60 + now.getMinutes()
|
||||
|
||||
// 将 startTime 和 endTime 转换为分钟数
|
||||
const startParts = startTime.split(':').map(Number)
|
||||
const endParts = endTime.split(':').map(Number)
|
||||
const startMinutes = startParts[0] * 60 + startParts[1]
|
||||
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 toMinutes = (t) => {
|
||||
const parts = t.split(':').map(Number)
|
||||
const h = parts[0] ?? 0
|
||||
const m = parts[1] ?? 0
|
||||
return h * 60 + m
|
||||
}
|
||||
const startMinutes = toMinutes(s)
|
||||
const endMinutes = toMinutes(e)
|
||||
|
||||
// 普通情况,直接判断时间是否在范围内
|
||||
if (endMinutes < startMinutes) {
|
||||
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
|
||||
const checkNotification = () => {
|
||||
// 遍历通知数组,找到第一个符合条件的通知
|
||||
for (let notification of notify.value) {
|
||||
// 判断时间是否符合当前时间
|
||||
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
|
||||
// 判断页面是否符合
|
||||
if (showPopup.value) return
|
||||
|
||||
if (isTimeValid && notification.notificationPage === route.path) {
|
||||
currentNotify.value = notification
|
||||
showPopup.value = true
|
||||
break // 只显示第一个符合的通知
|
||||
const matchedNotifications = []
|
||||
for (let notification of notify.value) {
|
||||
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
|
||||
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 = () => {
|
||||
showPopup.value = false
|
||||
onClosePopup()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useDialogStore } from "@/stores/dialogStore";
|
||||
import { useEnv } from "@/composables/useEnv";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useSEO } from "@/composables/useSEO";
|
||||
import { applyAppWebViewTokenFromRoute } from "@/bootstrap/appWebviewToken";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -123,6 +124,21 @@ const router = createRouter({
|
||||
import("@/views/UserAgreement.vue"),
|
||||
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",
|
||||
name: "agentManageAgreement",
|
||||
@@ -319,6 +335,42 @@ const router = createRouter({
|
||||
import("@/views/AgentServiceAgreement.vue"),
|
||||
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) => {
|
||||
NProgress.start(); // 启动进度条
|
||||
applyAppWebViewTokenFromRoute(to);
|
||||
const isAuthenticated = localStorage.getItem("token");
|
||||
const agentStore = useAgentStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
56
src/views/AccountCancelAgreement.vue
Normal file
56
src/views/AccountCancelAgreement.vue
Normal 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>
|
||||
@@ -105,86 +105,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活跃下级奖励 -->
|
||||
<!-- 团队奖励明细与下级 -->
|
||||
<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="flex items-center">
|
||||
<van-icon name="friends" class="text-xl mr-2" style="color: var(--color-success);" />
|
||||
<span class="text-lg font-bold" style="color: var(--van-text-color);">活跃下级奖励</span>
|
||||
</div>
|
||||
<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 }}
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<button type="button" @click="goToRewardsDetail"
|
||||
class="w-full rounded-full border bg-white/90 py-3 px-4 shadow-sm flex items-center justify-center"
|
||||
style="color: var(--van-text-color-2); border-color: var(--van-border-color);">
|
||||
<van-icon name="records" class="mr-1" />
|
||||
团队奖励明细
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
<button type="button" @click="toSubordinateList"
|
||||
class="w-full rounded-full py-3 px-4 shadow-md flex items-center justify-center text-white"
|
||||
style="background: linear-gradient(135deg, var(--color-success), var(--color-success-600));">
|
||||
<van-icon name="friends" class="mr-1" />
|
||||
查看我的下级
|
||||
@@ -219,33 +150,12 @@ const promoteDateOptions = [
|
||||
];
|
||||
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()
|
||||
@@ -264,7 +174,7 @@ onMounted(() => {
|
||||
|
||||
// 路由跳转
|
||||
const goToPromoteDetail = () => router.push({ name: "promoteDetails" });
|
||||
const goToActiveDetail = () => router.push({ name: "rewardsDetails" });
|
||||
const goToRewardsDetail = () => router.push({ name: "rewardsDetails" });
|
||||
|
||||
const toWithdraw = () => router.push({ name: "withdraw" });
|
||||
|
||||
|
||||
@@ -88,18 +88,9 @@ const getData = async () => {
|
||||
if (res.value?.code === 200 && !error.value) {
|
||||
if (page.value === 1) {
|
||||
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 {
|
||||
if (Array.isArray(res.value.data.list)) {
|
||||
const list = res.value.data.list.filter(
|
||||
i => i.type !== 'descendant_new_active' && i.type !== 'descendant_stay_active'
|
||||
)
|
||||
data.value.list.push(...list)
|
||||
data.value.list.push(...res.value.data.list)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -642,6 +642,13 @@ const buttonClass = computed(() => {
|
||||
// VIP价格配置
|
||||
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 baseOrders = 300 // 基础订单数
|
||||
@@ -741,20 +748,17 @@ onMounted(async () => {
|
||||
|
||||
const selectedType = ref('vip') // 默认选择VIP
|
||||
const showPayment = ref(false)
|
||||
const payData = ref({
|
||||
product_name: `${selectedType.value.toUpperCase()}代理`,
|
||||
sell_price: vipConfig.price,
|
||||
})
|
||||
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) {
|
||||
selectedType.value = type
|
||||
// 更新payData中的价格和产品名称
|
||||
payData.value = {
|
||||
product_name: `${type === 'vip' ? 'VIP' : 'SVIP'}代理`,
|
||||
sell_price: type === 'vip' ? vipConfig.price : vipConfig.svipPrice,
|
||||
}
|
||||
}
|
||||
|
||||
// 申请VIP或SVIP
|
||||
@@ -771,6 +775,11 @@ async function applyVip() {
|
||||
return
|
||||
}
|
||||
|
||||
if (toFiniteNumber(payData.value.sell_price) <= 0) {
|
||||
showToast('会员价格加载中,请稍后重试')
|
||||
return
|
||||
}
|
||||
|
||||
const { data, error } = await useApiFetch('/agent/membership/activate')
|
||||
.post({
|
||||
type: selectedType.value.toUpperCase(),
|
||||
|
||||
@@ -176,23 +176,14 @@ import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { showToast } from "vant";
|
||||
import { settings } from "nprogress";
|
||||
|
||||
// 报告类型选项
|
||||
const reportOptions = [
|
||||
{ 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 reportOptions = ref([]);
|
||||
|
||||
// 状态管理
|
||||
const showPicker = ref(false);
|
||||
const selectedReport = ref(reportOptions[0]);
|
||||
const selectedReportText = ref(reportOptions[0].text);
|
||||
const selectedReportId = ref(reportOptions[0].id);
|
||||
const selectedReport = ref(null);
|
||||
const selectedReportText = ref("");
|
||||
const selectedReportId = ref(null);
|
||||
|
||||
const configData = ref({});
|
||||
const productConfigData = ref({});
|
||||
@@ -347,6 +338,7 @@ const validateRatio = () => {
|
||||
|
||||
// 获取配置
|
||||
const getConfig = async () => {
|
||||
if (!selectedReportId.value) return;
|
||||
try {
|
||||
const { data, error } = await useApiFetch(
|
||||
"/agent/membership/user_config?product_id=" + selectedReportId.value
|
||||
@@ -474,9 +466,11 @@ const finalValidation = () => {
|
||||
|
||||
// 选择器确认
|
||||
const onConfirm = ({ selectedOptions }) => {
|
||||
selectedReport.value = selectedOptions[0];
|
||||
selectedReportText.value = selectedOptions[0].text;
|
||||
selectedReportId.value = selectedOptions[0].id;
|
||||
const picked = selectedOptions?.[0];
|
||||
if (!picked) return;
|
||||
selectedReport.value = picked;
|
||||
selectedReportText.value = picked.text;
|
||||
selectedReportId.value = picked.value;
|
||||
showPicker.value = false;
|
||||
// 重置错误状态
|
||||
rangeError.value = false;
|
||||
@@ -490,8 +484,32 @@ const closeRangeError = () => {
|
||||
rangeError.value = false;
|
||||
}, 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>
|
||||
|
||||
|
||||
278
src/views/CancelAccount.vue
Normal file
278
src/views/CancelAccount.vue
Normal 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>
|
||||
@@ -124,6 +124,15 @@
|
||||
</div>
|
||||
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
|
||||
</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"
|
||||
class="w-full flex items-center justify-between px-6 py-4 hover:bg-red-50 transition-colors"
|
||||
@click="handleLogout">
|
||||
@@ -142,13 +151,13 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref, computed, onBeforeMount } from "vue";
|
||||
import { showToast } from "vant";
|
||||
const router = useRouter();
|
||||
import headShot from "@/assets/images/head_shot.webp";
|
||||
import { useAgentStore } from "@/stores/agentStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import { useEnv } from "@/composables/useEnv";
|
||||
import { useDialogStore } from "@/stores/dialogStore";
|
||||
import useApiFetch from "@/composables/useApiFetch";
|
||||
const agentStore = useAgentStore();
|
||||
const userStore = useUserStore();
|
||||
const dialogStore = useDialogStore();
|
||||
@@ -235,22 +244,12 @@ function handleLogout() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function handleCancelAccount() {
|
||||
// 账号注销功能
|
||||
if (confirm("注销账号后,您的所有数据将被清除且无法恢复,确定要继续吗?")) {
|
||||
useApiFetch("/user/cancelOut")
|
||||
.post()
|
||||
.json()
|
||||
.then(({ data, error }) => {
|
||||
if (!error && data.value && data.value.code === 200) {
|
||||
alert("账号已注销");
|
||||
handleLogout();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("注销账号失败:", error);
|
||||
});
|
||||
function goCancelAccountPage() {
|
||||
if (!mobile.value) {
|
||||
showToast("请先绑定手机号");
|
||||
return;
|
||||
}
|
||||
router.push({ name: "cancelAccount" });
|
||||
}
|
||||
|
||||
function toService() {
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
: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>
|
||||
}}</span> 元</div>
|
||||
<div class="text-sm text-gray-500">我的成本为 <span class="text-orange-500">{{ costPrice
|
||||
}}</span> 元</div>
|
||||
}}</span> 元</div>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
@@ -87,16 +87,6 @@
|
||||
<script setup>
|
||||
import PriceInputPopup from '@/components/PriceInputPopup.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 showApplyPopup = ref(false); // 用来控制申请代理弹窗的显示
|
||||
const showPricePicker = ref(false);
|
||||
@@ -107,6 +97,16 @@ const selectedReportType = ref([]);
|
||||
const clientPrice = ref(null);
|
||||
const productConfig = ref(null);
|
||||
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();
|
||||
});
|
||||
const SelectTypePicker = (reportType) => {
|
||||
selectedReportType.value = [reportType];
|
||||
selectedReportType.value = [reportType.value];
|
||||
pickerFieldText.value = reportType.text;
|
||||
pickerFieldVal.value = reportType.value;
|
||||
for (let i of productConfig.value) {
|
||||
for (let i of (productConfig.value || [])) {
|
||||
if (i.product_id === reportType.id) {
|
||||
pickerProductConfig.value = i
|
||||
clientPrice.value = i.cost_price
|
||||
@@ -202,8 +202,16 @@ const getPromoteConfig = async () => {
|
||||
|
||||
if (data.value && !error.value) {
|
||||
if (data.value.code === 200) {
|
||||
productConfig.value = data.value.data.AgentProductConfig;
|
||||
SelectTypePicker(reportTypes[0])
|
||||
productConfig.value = data.value.data.AgentProductConfig || [];
|
||||
if (reportTypes.value.length > 0) {
|
||||
SelectTypePicker(reportTypes.value[0])
|
||||
} else {
|
||||
pickerProductConfig.value = null
|
||||
pickerFieldText.value = ''
|
||||
pickerFieldVal.value = null
|
||||
clientPrice.value = null
|
||||
showToast({ message: '暂无可推广产品' });
|
||||
}
|
||||
} else {
|
||||
console.log("Error fetching agent info", data.value);
|
||||
}
|
||||
|
||||
@@ -119,9 +119,7 @@ const fetchRewardDetails = async () => {
|
||||
}
|
||||
// 处理列表数据
|
||||
if (data.value.data.list) {
|
||||
const list = data.value.data.list.filter(
|
||||
i => i.type !== 'descendant_new_active' && i.type !== 'descendant_stay_active',
|
||||
)
|
||||
const list = data.value.data.list
|
||||
if (page.value === 1) {
|
||||
rewardDetails.value = list
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,12 @@ import rentalinfo from "@/assets/images/index/rentalinfo_bg.png";
|
||||
import bannerImg from "@/assets/images/index/banner_1.png";
|
||||
import bannerImg2 from "@/assets/images/index/banner_2.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) {
|
||||
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="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 class="text-center mt-1 font-bold ">推广报告</div>
|
||||
</div>
|
||||
@@ -123,7 +131,9 @@ const banners = ref([
|
||||
<div class="text-center flex flex-col justify-center items-center" @click="toInvitation">
|
||||
<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">
|
||||
<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 class="text-center mt-1 font-bold ">邀请下级</div>
|
||||
</div>
|
||||
@@ -137,7 +147,9 @@ const banners = ref([
|
||||
<div class="text-center flex flex-col justify-center items-center" @click="toHistory">
|
||||
<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">
|
||||
<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 class="text-center mt-1 font-bold ">我的报告</div>
|
||||
</div>
|
||||
@@ -195,7 +207,7 @@ const banners = ref([
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<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>
|
||||
<img src="@/assets/images/index/right.png" alt="右箭头" class="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user