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

5
.env
View File

@@ -1,7 +1,7 @@
VITE_API_URL=
VITE_API_PREFIX=/api/v1
VITE_COMPANY_NAME=海南省学宇思网络科技有限公司
VITE_COMPANY_NAME=海南海宇大数据有限公司
VITE_INQUIRE_AES_KEY=ff83609b2b24fc73196aac3d3dfb874f
@@ -13,4 +13,5 @@ VITE_CHAT_AES_IV=345GDFED433223DF
VITE_SHARE_TITLE=一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
VITE_SHARE_DESC=提供个人信用评估、入职背调、信贷风控、企业风险监测等服务
VITE_SHARE_IMG=https://www.tianyuandb.com/logo.png
VITE_SHARE_IMG=https://www.onecha.cn/logo.png
VITE_TOKEN_VERSION=1.5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,211 +1,324 @@
<script setup>
import { RouterLink, RouterView, useRouter } from "vue-router";
import { RouterView, useRouter, useRoute } from "vue-router";
const { isWeChat } = useEnv();
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useAuthStore } from "@/stores/authStore";
import { useAppStore } from "@/stores/appStore";
import { useWeixinShare } from "@/composables/useWeixinShare";
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
const router = useRouter();
const route = useRoute();
const agentStore = useAgentStore();
const userStore = useUserStore();
const dialogStore = useDialogStore();
const authStore = useAuthStore();
const { configWeixinShare, setDynamicShare } = useWeixinShare();
const appStore = useAppStore();
const { setDynamicShare } = useWeixinShare();
onMounted(() => {
// 检查token版本如果版本不匹配则清除旧token
checkTokenVersion()
onMounted(async () => {
// 初始化应用配置
await appStore.fetchAppConfig();
// 恢复微信授权状态(页面刷新
// 恢复微信授权状态(页面刷新或回调时
authStore.restoreFromStorage();
// 检查是否是微信授权回调
const url = new URL(window.location.href);
const hasWeixinCode = url.searchParams.has('code') && url.searchParams.has('state');
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
if (hasWeixinCode) {
// 如果是授权回调,标记为正在授权
authStore.startWeixinAuth();
if (code && state) {
// 这是微信授权回调,处理授权结果
console.log("Handling WeChat auth callback");
await handleWeixinAuthCallback(code);
} else {
// 正常初始化:加载用户信息等
await initializeApp();
}
getWeixinAuthUrl();
RefreshToken();
const token = localStorage.getItem("token");
if (token) {
agentStore.fetchAgentStatus();
userStore.fetchUserInfo();
}
// 配置微信分享
// 延迟执行确保微信SDK已加载
// 延迟配置微信分享
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 500);
}, 1000);
// 监听路由变化更新微信分享配置
router.afterEach(() => {
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 300);
// 监听路由变化更新分享配置
router.afterEach(async () => {
if (isWeChat.value && window.jWeixin && !authStore.isWeixinAuthing) {
await setDynamicShare();
}
});
});
const checkTokenVersion = () => {
// 设置新的token版本号当后端token格式改变时修改这个版本号
const CURRENT_TOKEN_VERSION = '2.1'
const storedTokenVersion = localStorage.getItem('tokenVersion')
/**
* 处理微信授权回调
*/
const handleWeixinAuthCallback = async (code) => {
try {
console.log("🔄 WeChat auth callback: code=", code);
if (!storedTokenVersion || storedTokenVersion !== CURRENT_TOKEN_VERSION) {
// 清除所有旧的认证信息
clearAuthData()
// 设置新的token版本
localStorage.setItem('tokenVersion', CURRENT_TOKEN_VERSION)
console.log('Token version updated, cleared old authentication data')
}
}
// 统一清除认证数据的工具函数
const clearAuthData = () => {
localStorage.removeItem('token')
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
}
const RefreshToken = async () => {
if (isWeChat.value) {
h5WeixinLogin();
return;
}
const token = localStorage.getItem("token");
const refreshAfter = localStorage.getItem("refreshAfter");
const accessExpire = localStorage.getItem("accessExpire");
const currentTime = new Date().getTime();
if (accessExpire) {
const accessExpireInMilliseconds = parseInt(accessExpire) * 1000; // 转换为毫秒级
if (currentTime > accessExpireInMilliseconds) {
if (isWeChat.value) {
h5WeixinLogin();
}
return;
}
}
// 1. 如果没有 token直接返回
if (!token) {
if (isWeChat.value) {
h5WeixinLogin();
}
return;
}
// 2. 如果有 refreshAfter检查当前时间是否超过 refreshAfterrefreshAfter 是秒级,需要转换为毫秒级)
if (refreshAfter) {
const refreshAfterInMilliseconds = parseInt(refreshAfter) * 1000; // 转换为毫秒级
if (currentTime < refreshAfterInMilliseconds) {
return;
}
}
// 3. 如果没有 refreshAfter 或者时间超过 refreshAfter执行刷新 token 的请求
refreshToken();
};
const mpWeixinLogin = () => { };
const refreshToken = async () => {
const { data, error } = await useApiFetch("/user/getToken").post().json();
if (data.value && !error.value) {
if (data.value.code === 200) {
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
localStorage.setItem("accessExpire", data.value.data.accessExpire);
}
}
};
const h5WeixinLogin = async () => {
// 获取当前URL
const url = new URL(window.location.href);
// 获取参数
const params = new URLSearchParams(url.search);
// 获取特定参数值
const code = params.get("code");
const state = params.get("state");
if (code && state) {
// 这是微信授权回调,处理授权结果
// 调用后端接口交换 token
const { data, error } = await useApiFetch("/user/wxh5Auth")
.post({ code })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem(
"refreshAfter",
data.value.data.refreshAfter
);
localStorage.setItem(
"accessExpire",
data.value.data.accessExpire
);
console.log("📡 wxh5Auth response:", {
code: data.value?.code,
hasToken: !!data.value?.data?.accessToken,
error: error.value
});
params.delete("code");
params.delete("state");
if (data.value && !error.value && data.value.code === 200) {
// 保存 token
const token = data.value.data.accessToken;
localStorage.setItem("token", token);
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
localStorage.setItem("accessExpire", data.value.data.accessExpire);
// ⚠️ 重要:保存 token 后立即设置 tokenVersion防止被 checkTokenVersion 清除
const tokenVersion = import.meta.env.VITE_TOKEN_VERSION || "1.1";
localStorage.setItem("tokenVersion", tokenVersion);
// 更新 URL不刷新页面
const newUrl = `${url.origin}${url.pathname
}?${params.toString()}`;
window.history.replaceState({}, "", newUrl);
console.log("✅ Token saved successfully, token:", token.substring(0, 20) + "...");
console.log("✅ Token saved to localStorage, userId:", data.value.data.userId || "unknown");
console.log(`✅ TokenVersion set to ${tokenVersion}`);
// 标记微信授权完成
authStore.completeWeixinAuth();
// 验证 token 是否真的保存成功
const savedToken = localStorage.getItem("token");
if (savedToken === token) {
console.log("✅ Token verification: localStorage中的token与保存的token一致");
} else {
console.error("❌ Token verification failed: localStorage中的token与保存的token不一致");
}
// 获取用户和代理信息
await Promise.all([
agentStore.fetchAgentStatus(),
userStore.fetchUserInfo()
]);
// 清除 URL 中的 code 和 state 参数
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("code");
params.delete("state");
const newUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""}`;
window.history.replaceState({}, "", newUrl);
console.log("✅ URL cleaned, removed code and state parameters");
// 如果有待处理的路由,跳转到该路由
if (authStore.pendingRoute) {
router.replace(authStore.pendingRoute);
authStore.clearPendingRoute();
// 获取用户信息
console.log("👤 Fetching user info...");
try {
await userStore.fetchUserInfo();
console.log("✅ User info fetched:", {
mobile: userStore.mobile,
isLoggedIn: userStore.isLoggedIn
});
} catch (userInfoErr) {
console.error("❌ Failed to fetch user info:", userInfoErr);
// 用户信息获取失败,清除 token跳转到登录
authStore.resetAuthState();
await router.replace("/login");
return;
}
// 获取代理信息(如果是有手机号的用户)
if (userStore.mobile) {
try {
await agentStore.fetchAgentStatus();
} catch (agentErr) {
console.warn("Warning: Failed to fetch agent status:", agentErr);
// 不中断流程,只是警告
}
}
// 标记授权完成
authStore.completeWeixinAuth();
console.log("✅ WeChat auth marked as complete");
// 获取 pendingRoute 并跳转
const pendingRoute = authStore.pendingRoute
console.log("🎯 pendingRoute:", pendingRoute);
// if (pendingRoute) {
// // ⚠️ 重要:必须先跳转再清除,否则清除后 pendingRoute 为 null
// console.log("🚀 Navigating to pendingRoute:", pendingRoute);
// await router.replace(pendingRoute);
// authStore.clearPendingRoute();
// console.log("✅ Navigated to pendingRoute and cleared it");
// } else {
// // 默认跳转到首页
// console.log("📍 No pendingRoute found, navigating to home");
// await router.replace("/");
// }
} else {
console.error("❌ WeChat auth failed:", {
code: data.value?.code,
message: data.value?.msg || data.value?.message,
error: error.value
});
// 授权失败,重置状态
authStore.resetAuthState();
// 跳转到登录页
await router.replace("/login");
}
} else {
// 没有授权参数,需要开始微信授权
// 保存当前路由作为授权完成后的目标路由
const currentRoute = router.currentRoute.value;
authStore.startWeixinAuth(currentRoute);
h5WeixinGetCode();
} catch (err) {
console.error("❌ Error handling WeChat auth callback:", err);
authStore.resetAuthState();
await router.replace("/login");
}
};
const h5WeixinGetCode = () => {
const currentUrl = window.location.href;
let redirectUri = encodeURIComponent(currentUrl);
let appId = import.meta.env.VITE_WECHAT_APP_ID;
let state = "snsapi_base";
let scope = "snsapi_base";
let authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
// 跳转到授权URL
window.location.href = authUrl;
/**
* 初始化应用:检查 token加载用户信息
*/
const initializeApp = async () => {
// 检查 token 版本
checkTokenVersion();
// 尝试刷新 token
await refreshTokenIfNeeded();
// 如果有 token加载用户信息
const token = localStorage.getItem("token");
if (token) {
try {
await userStore.fetchUserInfo();
if (userStore.mobile) {
await agentStore.fetchAgentStatus();
}
} catch (err) {
console.error("Error loading user info:", err);
}
}
};
/**
* 检查 token 版本,清除不兼容的旧 token
* ⚠️ 注意:只有在确实需要清除旧 token 时才清除,避免误清除新保存的 token
*/
const checkTokenVersion = () => {
const CURRENT_TOKEN_VERSION = import.meta.env.VITE_TOKEN_VERSION || "1.1";
const storedVersion = localStorage.getItem("tokenVersion");
const hasToken = !!localStorage.getItem("token");
// 如果 tokenVersion 不存在或版本不匹配
if (!storedVersion || storedVersion !== CURRENT_TOKEN_VERSION) {
// 只有在有 token 的情况下才清除(避免清除刚保存的新 token
if (hasToken) {
console.log(`Token version mismatch: storedVersion=${storedVersion}, currentVersion=${CURRENT_TOKEN_VERSION}, clearing old auth data`);
// Token 版本不匹配,清除旧数据
localStorage.removeItem("token");
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
localStorage.removeItem("userInfo");
localStorage.removeItem("agentInfo");
}
// 无论是否有 token都设置新的 tokenVersion
localStorage.setItem("tokenVersion", CURRENT_TOKEN_VERSION);
} else {
console.log(`Token version check passed: storedVersion=${storedVersion}, currentVersion=${CURRENT_TOKEN_VERSION}`);
}
};
/**
* 在需要时刷新 token
*/
const refreshTokenIfNeeded = async () => {
const token = localStorage.getItem("token");
if (!token) return;
const accessExpire = localStorage.getItem("accessExpire");
const refreshAfter = localStorage.getItem("refreshAfter");
const now = Date.now();
// 检查 token 是否已过期
if (accessExpire) {
const expireTime = parseInt(accessExpire) * 1000;
if (now > expireTime) {
console.log("Token expired");
return; // Token 已过期,不刷新,由路由守卫处理
}
}
// 检查是否需要刷新
if (refreshAfter) {
const refreshTime = parseInt(refreshAfter) * 1000;
if (now < refreshTime) {
return; // 还不需要刷新
}
}
// 执行 token 刷新
try {
const { data, error } = await useApiFetch("/user/getToken").post().json();
if (data.value && !error.value && data.value.code === 200) {
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
localStorage.setItem("accessExpire", data.value.data.accessExpire);
console.log("Token refreshed successfully");
}
} catch (err) {
console.error("Error refreshing token:", err);
}
};
/**
* 发起微信授权 URL
* 这个逻辑已经在路由守卫中实现了
* 这里保留这个函数的备份,以防需要其他地方调用
*/
const getWeixinAuthUrl = () => {
const isAuthenticated = localStorage.getItem("token");
// 检查 token 是否过期
const accessExpire = localStorage.getItem("accessExpire");
const now = Date.now();
let isTokenExpired = false;
if (accessExpire) {
isTokenExpired = now > parseInt(accessExpire) * 1000;
}
console.log("WeChat auth check:", {
isWeChat: isWeChat.value,
isAuthenticated,
isTokenExpired
});
if (isWeChat.value && !isAuthenticated && !isTokenExpired) {
console.log("🔄 Initiating WeChat auth flow");
// 如果正在授权中或已完成授权,则阻止重复授权
console.log("Auth store state:", {
isWeixinAuthing: authStore.isWeixinAuthing,
weixinAuthComplete: authStore.weixinAuthComplete
});
if (authStore.isWeixinAuthing || authStore.weixinAuthComplete) {
return;
}
// 保存目标路由
authStore.startWeixinAuth(route);
console.log("🔖 Saved pendingRoute for WeChat auth:", route.fullPath);
const appId = import.meta.env.VITE_WECHAT_APP_ID;
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("code");
params.delete("state");
const cleanUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""
}`;
const redirectUri = encodeURIComponent(cleanUrl);
const weixinAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=snsapi_base#wechat_redirect`;
console.log(
"🔄 Triggering WeChat auth from route guard, pendingRoute:",
route.fullPath
);
window.location.href = weixinAuthUrl;
}
};
</script>
<template>
<RouterView />
<BindPhoneDialog />
<BindPhoneOnlyDialog />
</template>
<style scoped></style>

280
src/api/agent.js Normal file
View File

@@ -0,0 +1,280 @@
import useApiFetch from "@/composables/useApiFetch";
/**
* 代理相关API调用统一管理
*/
/**
* 构建查询字符串的辅助函数
* @param {object} params - 查询参数对象
* @returns {string} 查询字符串
*/
function buildQueryString(params) {
const queryParams = new URLSearchParams();
Object.keys(params).forEach((key) => {
if (
params[key] !== undefined &&
params[key] !== null &&
params[key] !== ""
) {
queryParams.append(key, params[key]);
}
});
const queryString = queryParams.toString();
return queryString ? `?${queryString}` : "";
}
// ==================== 公开接口(无需登录) ====================
/**
* 获取推广链接数据
* @param {string} linkIdentifier - 推广链接标识
*/
export function getLinkData(linkIdentifier) {
return useApiFetch(
`/agent/link?link_identifier=${encodeURIComponent(linkIdentifier)}`
)
.get()
.json();
}
/**
* 通过邀请码申请成为代理(已注册用户)
* @param {object} params - 申请参数
* @param {string} params.mobile - 手机号
* @param {string} params.code - 验证码
* @param {string} params.invite_code - 邀请码(必填)
* @param {string} params.region - 区域(可选)
*/
export function applyForAgent(params) {
return useApiFetch("/agent/apply").post(params).json();
}
/**
* 通过邀请码注册(同时注册用户和代理)
* @param {object} params - 注册参数
* @param {string} params.mobile - 手机号
* @param {string} params.code - 验证码
* @param {string} params.invite_code - 邀请码(必填)
* @param {string} params.region - 区域(可选)
* @param {string} params.wechat_id - 微信号(可选)
*/
export function registerByInviteCode(params) {
return useApiFetch("/agent/register/invite").post(params).json();
}
// ==================== 需要登录的接口 ====================
/**
* 获取代理信息
*/
export function getAgentInfo() {
return useApiFetch("/agent/info").get().json();
}
/**
* 获取代理等级特权信息
*/
export function getLevelPrivilege() {
return useApiFetch("/agent/level/privilege").get().json();
}
/**
* 生成推广链接
* @param {object} params - 生成参数
* @param {number} params.product_id - 产品ID
* @param {number} params.set_price - 设定价格
*/
export function generateLink(params) {
return useApiFetch("/agent/generating_link").post(params).json();
}
/**
* 获取产品配置
*/
export function getProductConfig() {
return useApiFetch("/agent/product_config").get().json();
}
/**
* 获取团队统计
*/
export function getTeamStatistics() {
return useApiFetch("/agent/team/statistics").get().json();
}
/**
* 获取团队列表
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
*/
export function getTeamList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/team/list${queryString}`).get().json();
}
/**
* 获取下级列表
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
*/
export function getSubordinateList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/subordinate/list${queryString}`).get().json();
}
/**
* 获取收益信息
*/
export function getRevenueInfo() {
return useApiFetch("/agent/revenue").get().json();
}
/**
* 获取转化率统计
*/
export function getConversionRate() {
return useApiFetch("/agent/conversion/rate").get().json();
}
/**
* 获取佣金记录
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
*/
export function getCommissionList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/commission/list${queryString}`).get().json();
}
/**
* 获取返佣记录(推广返佣)
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
* @param {number} params.rebate_type - 返佣类型可选1=直接上级返佣2=钻石上级返佣3=黄金上级返佣
*/
export function getRebateList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/rebate/list${queryString}`).get().json();
}
/**
* 获取升级返佣记录
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
*/
export function getUpgradeRebateList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/rebate/upgrade/list${queryString}`).get().json();
}
/**
* 获取升级记录
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
*/
export function getUpgradeList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/upgrade/list${queryString}`).get().json();
}
/**
* 申请升级
* @param {object} params - 升级参数
* @param {number} params.to_level - 目标等级2=黄金3=钻石
*/
export function applyUpgrade(params) {
return useApiFetch("/agent/upgrade/apply").post(params).json();
}
/**
* 钻石代理升级下级
* @param {object} params - 升级参数
* @param {number} params.subordinate_id - 下级代理ID
* @param {number} params.to_level - 目标等级只能是2=黄金)
*/
export function upgradeSubordinate(params) {
return useApiFetch("/agent/upgrade/subordinate").post(params).json();
}
/**
* 获取提现列表
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
*/
export function getWithdrawalList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/withdrawal/list${queryString}`).get().json();
}
/**
* 申请提现
* @param {object} params - 提现参数
* @param {number} params.amount - 提现金额
* @param {string} params.payee_account - 收款账户
* @param {string} params.payee_name - 收款人姓名
*/
export function applyWithdrawal(params) {
return useApiFetch("/agent/withdrawal/apply").post(params).json();
}
/**
* 实名认证
* @param {object} params - 实名认证参数
* @param {string} params.name - 姓名
* @param {string} params.id_card - 身份证号
* @param {string} params.mobile - 手机号
* @param {string} params.code - 验证码
*/
export function realNameAuth(params) {
return useApiFetch("/agent/real_name").post(params).json();
}
/**
* 生成邀请码
* @param {object} params - 生成参数
* @param {number} params.count - 生成数量
* @param {number} params.expire_days - 过期天数可选0表示不过期
* @param {string} params.remark - 备注(可选)
*/
export function generateInviteCode(params) {
return useApiFetch("/agent/invite_code/generate").post(params).json();
}
/**
* 获取邀请码列表
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
* @param {number} params.status - 状态可选0=未使用1=已使用2=已失效
*/
export function getInviteCodeList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/invite_code/list${queryString}`).get().json();
}
/**
* 删除邀请码
* @param {object} params - 删除参数
* @param {number} params.id - 邀请码ID
*/
export function deleteInviteCode(params) {
return useApiFetch("/agent/invite_code/delete").post(params).json();
}
/**
* 获取邀请链接
* @param {object} params - 请求参数
* @param {string} params.invite_code - 邀请码
*/
export function getInviteLink(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/invite_link${queryString}`).get().json();
}

View File

@@ -1,8 +1,24 @@
import axios from "axios";
import useApiFetch from "@/composables/useApiFetch";
// 获取API基础URL与生产规则一致VITE_API_URL
const baseURL = import.meta.env.VITE_API_URL;
// 手机号验证码登录
export function mobileCodeLogin(params) {
return useApiFetch("/user/mobileCodeLogin").post(params).json();
}
// 统一认证
export function unifiedAuth(params) {
return useApiFetch("/user/auth").post(params).json();
}
// 绑定手机号
export function bindMobile(params) {
return useApiFetch("/user/bindMobile").post(params).json();
}
// 注销账号API
export function cancelAccount() {
return axios({

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

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="1764650175158" class="icon" viewBox="0 0 1052 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14882" xmlns:xlink="http://www.w3.org/1999/xlink" width="205.46875" height="200"><path d="M516.920889 741.745778c4.010667 0 8.106667-0.227556 12.231111-0.711111 39.395556-4.551111 72.874667-31.260444 85.248-68.010667a5.575111 5.575111 0 0 0-3.128889-6.912 5.973333 5.973333 0 0 0-7.480889 2.275556c-0.142222 0.284444-15.815111 24.746667-76.8 32.711111-9.102222 1.194667-17.976889 1.820444-26.396444 1.820444-42.723556 0-61.781333-15.616-61.952-15.758222a5.973333 5.973333 0 0 0-7.623111-0.085333 5.575111 5.575111 0 0 0-1.223111 7.338666 103.964444 103.964444 0 0 0 87.125333 47.331556zM844.8 375.808c-0.540444 0-1.024 0.113778-1.536 0.113778C807.537778 226.133333 671.004444 114.346667 507.335111 114.346667c-168.163556 0-307.797333 117.930667-338.773333 273.92-30.094222 5.233778-52.992 30.833778-52.992 61.809777v124.017778c0 34.702222 28.785778 62.919111 64.284444 62.919111 20.053333 0 37.745778-9.187556 49.521778-23.239111 28.444444 72.817778 86.328889 131.527111 159.573333 162.503111a32.995556 32.995556 0 0 1 3.242667-5.034666c1.137778-1.450667 2.446222-2.645333 3.555556-2.645334 1.137778 0 2.190222 0.398222 3.100444 1.024-16.952889-12.231111-78.222222-75.064889-91.477333-162.759111-5.802667-38.570667 24.035556-76.458667 58.823111-82.744889 55.808-10.069333 111.331556-21.532444 167.139555-31.402666 35.470222-6.257778 59.733333-25.116444 74.524445-56.490667 3.498667-7.338667 8.533333-22.186667 10.808889-43.548444a6.826667 6.826667 0 0 1 6.769777-5.632c2.275556 0 4.266667 1.137778 5.546667 2.759111l1.536-0.910223c21.987556 30.919111 65.621333 99.413333 71.879111 171.633778 7.196444 82.545778 3.185778 139.093333-62.065778 196.949334l-0.284444 0.227555a5.12 5.12 0 0 0-1.422222 3.555556c0 1.763556 0.967111 3.299556 2.389333 4.209777 0.540444 0.227556 1.080889 0.512 1.621333 0.711112 0.426667 0.085333 0.853333 0.227556 1.28 0.227555 0.455111 0 0.853333-0.142222 1.223112-0.227555 0.938667-0.483556 1.820444-1.024 2.730666-1.479112 65.991111-35.214222 116.622222-94.008889 140.003556-164.721777 9.472 13.624889 24.291556 23.409778 41.528889 26.851555-27.619556 121.656889-139.690667 197.831111-278.528 209.379556-8.305778-19.996444-28.785778-34.190222-52.849778-34.190222-31.431111 0-56.888889 24.092444-56.888889 53.788444s25.457778 53.76 56.888889 53.76c25.315556 0 46.535111-15.758222 53.902222-37.404444 160.711111-12.629333 289.536-105.159111 316.416-248.888889 23.779556-9.614222 40.448-32.170667 40.448-58.538667v-125.44c0-35.072-29.553778-63.516444-65.991111-63.516444z m-59.278222 36.039111c-41.927111-109.966222-150.528-188.501333-278.300445-188.501333-127.232 0-235.463111 77.880889-277.76 187.136-2.104889-2.503111-4.579556-4.636444-6.997333-6.798222 23.950222-133.12 142.250667-234.353778 284.899556-234.353778 141.966222 0 259.896889 100.238222 284.643555 232.419555a62.094222 62.094222 0 0 0-6.485333 10.097778zM389.091556 776.334222h-0.028445c-0.170667 0.341333-0.170667 0.398222 0.028445 0z" fill="#1daaf4" p-id="14883"></path></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

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="1764650020736" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10374" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M700.02368 966.144H324.85568a140.8 140.8 0 0 1-130.24-90.368L57.46368 516.928a70.784 70.784 0 0 1 12.8-140.288 70.4 70.4 0 0 1 69.632 65.792 605.696 605.696 0 0 0 108.096 13.184 168.448 168.448 0 0 0 79.936-16.64c64-33.792 105.472-121.984 135.936-186.368l2.368-5.056 0.384-0.896a94.784 94.784 0 0 1-47.296-82.112 93.12 93.12 0 1 1 139.072 81.92v0.512a410.816 410.816 0 0 0 138.496 182.208 199.168 199.168 0 0 0 94.208 19.2 724.992 724.992 0 0 0 94.016-8.064 69.888 69.888 0 1 1 82.24 76.8l-137.152 358.784a140.8 140.8 0 0 1-130.176 90.24z m-212.8-312.192v178.24a33.28 33.28 0 1 0 66.56 0v-178.304l29.568 29.952a38.4 38.4 0 0 0 55.104 0 39.936 39.936 0 0 0 0-55.808L548.02368 536.448a38.4 38.4 0 0 0-24.704-11.392h-5.696a38.976 38.976 0 0 0-24.768 11.456l-90.304 91.52a39.936 39.936 0 0 0 0 55.808 38.4 38.4 0 0 0 55.168 0l29.44-29.824z" p-id="10375" fill="#1daaf4"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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="1764650035745" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11504" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M733.6448 947.2c-118.1184 0-213.6064-101.504-213.6064-226.5344 0-125.2608 95.6928-226.5856 213.6064-226.5856C851.456 494.08 947.2 595.584 947.2 720.6656 947.2 845.8752 851.6352 947.2 733.6448 947.2z m51.0976-316.8l-81.8432 101.6576v-101.6576h-66.3296v17.536h16.5376v193.3824h49.7152l164.7616-210.7392-82.7648-0.2048h-0.0768z m-184.5504-146.9952l-6.4512 4.224a217.216 217.216 0 0 1-115.9424 33.536c-124.4416 0-225.152-105.216-225.152-235.008C252.6464 156.3392 353.408 51.2 477.7472 51.2c124.4416 0 225.2032 105.216 225.2032 234.9312a238.2848 238.2848 0 0 1-26.9056 111.616 235.8784 235.8784 0 0 1-75.8528 85.6576z m-75.52 72.2944a288.3072 288.3072 0 0 0-51.456 164.9664c0 37.1968 6.9376 73.3952 20.5312 107.4688A274.4832 274.4832 0 0 0 584.4736 947.2h-191.1296C232.704 947.2 102.4 947.2 102.4 878.6688v-19.456c0-167.552 130.304-303.5136 290.944-303.5136h131.328z" p-id="11505" fill="#1daaf4"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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="1764649808201" class="icon" viewBox="0 0 1169 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3433" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.3203125" height="200"><path d="M72.227662 0h996.553234c17.168557 1.082587 33.980498 7.794627 46.780497 19.359204 16.162388 14.239204 25.7401 35.547065 25.6 57.084179 0.012736 167.915622 0.025473 335.831244 0 503.734129-25.803781-31.751642-58.192239-58.141294-94.605373-76.812736-37.686766-19.435622-79.627463-30.47801-121.988458-32.261094-49.620697-2.228856-99.776318 8.227662-144.238806 30.401592-37.393831 18.531343-70.737512 45.11204-97.216318 77.360398-29.497313 35.827264-50.384876 78.697711-60.255523 124.039005-14.277413 64.751443-6.075224 134.240796 23.294727 193.719403-189.911244 0.025473-379.809751 0.012736-569.708259 0.012736-21.549851 0.127363-42.883184-9.450348-57.096915-25.625473C7.8201 858.249552 1.146269 841.501294 0 824.409154V72.40597C1.120796 54.345871 8.54607 36.680597 21.167761 23.638607 34.38806 9.628657 53.046766 1.235423 72.227662 0m240.487164 132.190249c-28.64398 3.362388-56.07801 16.162388-77.054726 35.954627-22.530547 20.96398-37.623085 49.735323-41.927961 80.213333-4.444975 30.274229 1.630249 61.974925 17.104876 88.39005 16.862886 29.153433 44.857313 51.671244 77.029254 61.707462 31.369552 9.99801 66.330746 8.138507 96.41393-5.22189 30.121393-13.169353 55.071841-37.635821 68.890746-67.451542 14.442985-30.681791 16.71005-66.827463 6.393632-99.126767-9.488557-30.363383-30.108657-56.995025-56.918607-74.061691-26.415124-16.926567-58.790846-24.275423-89.931144-20.403582m333.475025 31.471443c-6.457313 2.088756-11.984876 6.852139-14.965174 12.940099-3.960995 7.845572-3.413333 17.741692 1.426467 25.077811 4.63602 7.374328 13.271244 11.895721 21.97015 11.679204 109.978109 0.025473 219.968955 0.025473 329.947064 0.012736 8.698905 0.216517 17.346866-4.304876 22.021095-11.666467 5.196418-7.883781 5.412935-18.684179 0.522189-26.771742-4.534129-7.832836-13.475025-12.774527-22.517811-12.507064-107.851144 0.012736-215.689552 0-323.52796 0-4.9799 0.038209-10.0999-0.38209-14.87602 1.235423m0.063681 110.793233c-6.775721 2.152438-12.545274 7.29791-15.436418 13.80617-3.642587 7.947463-2.763781 17.754428 2.279801 24.899502 4.725174 6.966766 13.080199 11.195224 21.498906 11.004179 110.436617 0 220.873234 0.076418 331.309851-0.038209 14.035423 0.012736 26.134925-13.334925 24.670248-27.319403-0.598607-9.832438-7.501692-18.837015-16.773731-22.084776-5.005373-1.897711-10.443781-1.464677-15.678408-1.490149H661.027662c-4.941692 0.038209-10.010746-0.38209-14.77413 1.222686M214.199403 476.605771c-30.516219 23.460299-53.925572 55.976119-66.636418 92.3001-12.507065 35.648955-14.557612 74.851343-6.024279 111.63383 130.101493 0.025473 260.190249-0.025473 390.291742 0.025473 9.552239-40.794428 5.896915-84.581891-10.418309-123.172935-16.95204-40.450547-47.646567-74.940498-85.804577-96.515821-32.286567-18.505871-69.858706-27.53592-107.010547-25.931144-41.252935 1.464677-81.843582 16.315224-114.397612 41.660497z" p-id="3434" fill="#1daaf4"></path><path d="M882.028259 511.694328c50.066468-5.84597 101.788657 3.247761 146.658706 26.300498 39.75005 20.3399 74.061692 51.2 98.413533 88.619303 21.231443 32.464876 34.922985 69.795025 39.60995 108.296916 5.514826 44.296915-0.611343 90.045771-17.970946 131.196816-21.129552 50.512239-58.943682 93.85393-106.144477 121.63184-32.65592 19.410149-69.74408 31.433234-107.609154 34.668259-44.742687 4.126567-90.542488-3.782687-131.273234-22.772537-37.228259-17.346866-70.202587-43.711045-95.254926-76.277811-27.29393-35.317811-45.137512-77.882587-50.983482-122.141294-6.355423-47.022488 0.458507-95.789851 19.906866-139.093333 18.505871-41.58408 48.359801-77.971741 85.384278-104.399602 35.012139-25.090547 76.456119-41.125572 119.262886-46.029055m144.633632 157.051543c-5.056318 1.69393-8.877214 5.553035-12.481592 9.297512-41.813333 41.813333-83.61393 83.626667-125.44 125.414527-26.020299-25.994826-52.027861-52.015124-78.035423-78.022686-3.222289-3.209552-6.317214-6.724776-10.469254-8.737115-7.705473-3.973731-17.512438-3.324179-24.606567 1.642986-5.553035 4.024677-10.341891 9.666866-11.704677 16.557213-1.872239 8.100299 0.904279 16.977512 6.915821 22.708856 33.52199 33.509254 67.018507 67.056716 100.578706 100.540498 6.406368 6.457313 16.56995 8.660697 25.103284 5.553035 6.266269-2.037811 10.405572-7.412537 15.385473-11.386269 47.009751-46.882388 93.904876-93.904876 140.889154-140.812736 3.515224-3.375124 6.32995-7.578109 7.463483-12.366966 5.234627-18.467662-15.652935-37.686766-33.598408-30.388855z" p-id="3435" fill="#1daaf4"></path></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

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="1765182453848" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2537" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0h1024v1024H0z" fill="#1daaf4" fill-opacity="0" p-id="2538"></path><path d="M668 484c121.503 0 220 98.497 220 220 0 42.679-12.153 82.52-33.187 116.25l54.531 54.532c19.527 19.526 19.527 51.184 0 70.71-19.33 19.331-50.552 19.525-70.12 0.58l-0.59-0.58-54.567-54.565C750.376 911.89 710.602 924 668 924c-121.503 0-220-98.497-220-220s98.497-220 220-220zM768 64c17.673 0 32.004 14.327 32.004 32v356.477C760.566 431.737 715.654 420 668 420c-156.849 0-284 127.151-284 284 0 112.742 65.694 210.14 160.891 256.003H192c-17.673 0-32-14.33-32-32.003V428h268c53.02 0 96-42.98 96-96V64h244zM668 584c-66.274 0-120 53.726-120 120s53.726 120 120 120 120-53.726 120-120-53.726-120-120-120zM460 64v268c0 17.673-14.327 32-32 32H160L460 64z" fill="#1daaf4" p-id="2539"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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="1764649823438" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4596" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M922.122705 347.051227c0-19.764136-16.021905-35.785018-35.785018-35.785018L99.064226 311.26621c-19.764136 0-35.785018 16.020882-35.785018 35.785018l0 533.198811c0 19.763113 16.020882 35.785018 35.785018 35.785018l787.273461 0c19.763113 0 35.785018-16.021905 35.785018-35.785018L922.122705 744.266971 696.67607 744.266971l0-0.022513c-0.596588 0.008186-1.191128 0.022513-1.788739 0.022513-72.136947 0-130.615826-58.478879-130.615826-130.615826s58.478879-130.615826 130.615826-130.615826c0.598634 0 1.193175 0.014326 1.788739 0.022513l0-0.022513 225.446635 0L922.122705 347.051227zM617.949031 613.651145c0 42.491767 34.446533 76.9383 76.9383 76.9383s76.9383-34.446533 76.9383-76.9383-34.446533-76.9383-76.9383-76.9383S617.949031 571.159378 617.949031 613.651145zM836.231499 277.968849l-44.421722-155.503657c-4.469801-15.647375-20.828374-24.724101-36.537148-20.273742L134.930085 277.968849 836.231499 277.968849z" fill="#1daaf4" p-id="4597"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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="1764650051703" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12625" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M775.912727 105.192727H234.356364a69.818182 69.818182 0 0 0-68.654546 68.654546V837.818182a69.818182 69.818182 0 0 0 68.654546 69.818182h384a226.210909 226.210909 0 0 0 226.210909-226.210909V173.847273a69.818182 69.818182 0 0 0-68.654546-68.654546zM541.789091 577.396364H302.545455a18.385455 18.385455 0 0 1 0-36.770909h239.243636a18.385455 18.385455 0 1 1 0 36.770909z m165.236364-128.465455H302.545455a18.385455 18.385455 0 0 1 0-36.770909h404.48a18.385455 18.385455 0 1 1 0 36.770909z m0-128.465454H302.545455a18.385455 18.385455 0 0 1 0-36.77091h404.48a18.385455 18.385455 0 1 1 0 36.77091z" fill="#1daaf4" p-id="12626"></path></svg>

After

Width:  |  Height:  |  Size: 971 B

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="1764649988299" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6624" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M714.5 501.2c0 19.5-16 35.4-35.4 35.4H345.7c-19.4 0-35.4-16-35.4-35.4 0-19.5 16-35.4 35.4-35.4h333.4c19.5 0 35.4 15.8 35.4 35.4z m244.7-133.4V261.7c0-29.7-11.7-58.2-32.7-79.2-21-21-49.5-32.9-79.2-32.7H176.8c-29.7 0-58.2 11.8-79.1 32.7-21 21-32.7 49.5-32.7 79.2v106.1c82.3 0 149 65.1 149 145.3S147.3 658.4 65 658.4v106c0 29.7 11.7 58.2 32.7 79.2 21 21 49.5 32.9 79.2 32.7h670.6c29.7 0 58.2-11.8 79.1-32.7 21-21 32.7-49.5 32.7-79.2v-106c-82.3 0-149-65.1-149-145.3-0.1-80.4 66.7-145.3 148.9-145.3z" fill="#1daaf4" p-id="6625"></path></svg>

After

Width:  |  Height:  |  Size: 869 B

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="1764650162537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13739" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 384m-85.333333 0a85.333333 85.333333 0 1 0 170.666666 0 85.333333 85.333333 0 1 0-170.666666 0Z" p-id="13740" fill="#1daaf4"></path><path d="M768 56.888889H256a113.777778 113.777778 0 0 0-113.777778 113.777778v682.666666a113.777778 113.777778 0 0 0 113.777778 113.777778h369.777778a256 256 0 0 0 256-256V170.666667a113.777778 113.777778 0 0 0-113.777778-113.777778z m-142.222222 554.666667a42.666667 42.666667 0 0 1 0 85.333333h-71.111111V768a42.666667 42.666667 0 0 1-85.333334 0v-219.022222a170.666667 170.666667 0 1 1 85.333334 0v62.577778z" p-id="13741" fill="#1daaf4"></path></svg>

After

Width:  |  Height:  |  Size: 925 B

View File

@@ -119,6 +119,7 @@ declare global {
const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useAppStore: typeof import('./stores/appStore.js')['useAppStore']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,9 @@ import { useAgentStore } from "@/stores/agentStore";
// 创建全局的 fetch 实例
const useApiFetch = createFetch({
// 统一从环境变量读取域名与前缀(不提供默认值)
baseUrl: `${import.meta.env.VITE_API_URL}${import.meta.env.VITE_API_PREFIX}`,
baseUrl: `${import.meta.env.VITE_API_URL}${
import.meta.env.VITE_API_PREFIX
}`,
options: {
async beforeFetch({ url, options }) {
showLoadingToast({
@@ -28,13 +30,24 @@ const useApiFetch = createFetch({
if (isWechat) {
platform = "wxh5";
}
options.headers['X-Platform'] = platform
options.headers["X-Platform"] = platform;
if (token) {
options.headers = {
...options.headers,
Authorization: `${token}`,
};
console.log(
`[useApiFetch] 请求 ${url}, 携带token, token长度: ${
token.length
}, token前20字符: ${token.substring(0, 20)}...`
);
} else {
console.warn(
`[useApiFetch] 请求 ${url}, 没有token, localStorage中token为: ${localStorage.getItem(
"token"
)}`
);
}
return { url, options };
},
@@ -44,30 +57,35 @@ const useApiFetch = createFetch({
if (response.status === 401) {
// 清除本地存储的 token
localStorage.removeItem("token");
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
// 跳转到登录页
router.replace("/login");
}
if (data.code !== 200) {
if (data.code === 100009) {
if (data.code === 100009 || data.code === 100008) {
// 改进的存储管理
localStorage.removeItem('token')
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
localStorage.removeItem("token");
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
localStorage.removeItem("userInfo");
localStorage.removeItem("agentInfo");
localStorage.removeItem("tokenVersion");
// 重置状态
const userStore = useUserStore();
const agentStore = useAgentStore();
userStore.resetUser()
agentStore.resetAgent()
location.reload()
userStore.resetUser();
agentStore.resetAgent();
location.reload();
}
if (data.code !== 200002 && data.code !== 200003 && data.code !== 200004 && data.code !== 100009) {
if (
data.code !== 200002 &&
data.code !== 200003 &&
data.code !== 200004 &&
data.code !== 100009
) {
showToast({ message: data.msg });
}
}
@@ -76,11 +94,11 @@ const useApiFetch = createFetch({
async onFetchError({ error, response }) {
console.log("error", error);
closeToast();
if (response.status === 401) {
if (response && response.status === 401) {
// 清除本地存储的 token
localStorage.removeItem("token");
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
// 跳转到登录页
router.replace("/login");
} else {

View File

@@ -20,17 +20,17 @@
</div>
<div class="disclaimer">
<div class="flex flex-col items-center">
<div class="flex items-center">
<!-- <div class="flex items-center">
<img class="w-4 h-4 mr-2" src="@/assets/images/public_security_record_icon.png" alt="公安备案" />
<text>琼公网安备46010002000584号</text>
</div>
</div> -->
<div>
<a class="text-blue-500" href="https://beian.miit.gov.cn">
琼ICP备2024048057-2
琼ICP备2024038584-10
</a>
</div>
</div>
<div>海南省学宇思网络科技有限公司版权所有</div>
<div>海南海宇大数据有限公司版权所有</div>
</div>
</div>
</template>
@@ -43,8 +43,8 @@ const router = useRouter();
const route = useRoute();
const tabbar = ref("promote");
const menu = reactive([
{ title: "推广", icon: "cluster-o", name: "promote" },
{ title: "资产", icon: "gold-coin-o", name: "agent" },
{ title: "首页", icon: "home-o", name: "promote" },
{ title: "数据", icon: "bar-chart-o", name: "agent" },
{ title: "我的", icon: "user-o", name: "me" }
]);
// 根据当前路由设置 Tabbar 的高亮项
@@ -83,7 +83,7 @@ const tabChange = (name, a, b, c) => {
// 跳转到投诉页面
const toComplaint = () => {
window.location.href =
"https://work.weixin.qq.com/kfid/kfc8a32720024833f57"; // 跳转到客服页面
"https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3"; // 跳转到客服页面
// router.push({ name: 'complaint' }); // 使用 Vue Router 进行跳转
};
</script>

View File

@@ -1,17 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import NProgress from 'nprogress'
import GlobalLayout from '@/layouts/GlobalLayout.vue'
import HomeLayout from '@/layouts/HomeLayout.vue'
import PageLayout from '@/layouts/PageLayout.vue'
import index from '@/views/index.vue'
import Promote from '@/views/Promote.vue'
import PromotePage from '@/views/PromotePage.vue'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useEnv } from '@/composables/useEnv'
import { storeToRefs } from 'pinia'
import { useSEO } from '@/composables/useSEO'
import { createRouter, createWebHistory } from "vue-router";
import NProgress from "nprogress";
import GlobalLayout from "@/layouts/GlobalLayout.vue";
import HomeLayout from "@/layouts/HomeLayout.vue";
import PageLayout from "@/layouts/PageLayout.vue";
import index from "@/views/index.vue";
import Promote from "@/views/Promote.vue";
import PromotePage from "@/views/PromotePage.vue";
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from "@/composables/useEnv";
import { storeToRefs } from "pinia";
import { useSEO } from "@/composables/useSEO";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -27,262 +27,292 @@ const router = createRouter({
},
routes: [
{
path: '/',
path: "/",
component: GlobalLayout, // 使用 Layout 作为父组件
children: [
{
path: '',
path: "",
component: HomeLayout, // 使用 Layout 作为父组件
children: [
{
path: '',
name: 'promote',
path: "",
name: "promote",
component: PromotePage,
},
{
path: 'index',
name: 'index',
path: "index",
name: "index",
component: index,
},
{
path: '/agent',
name: 'agent',
component: () => import('@/views/Agent.vue'),
path: "/agent",
name: "agent",
component: () => import("@/views/Agent.vue"),
meta: { title: "代理主页", requiresAuth: true },
},
{
path: 'me',
name: 'me',
component: () => import('@/views/Me.vue'),
path: "me",
name: "me",
component: () => import("@/views/Me.vue"),
},
],
},
{
path: '',
path: "",
component: PageLayout,
children: [
{
path: '/historyQuery',
name: 'history',
component: () => import('@/views/HistoryQuery.vue'),
meta: { title: '历史报告', requiresAuth: true },
},
{
path: '/help',
name: 'help',
component: () => import('@/views/Help.vue'),
meta: { title: '帮助中心' },
},
{
path: '/help/detail',
name: 'helpDetail',
component: () => import('@/views/HelpDetail.vue'),
meta: { title: '帮助中心' },
},
{
path: '/help/guide',
name: 'helpGuide',
component: () => import('@/views/HelpGuide.vue'),
meta: { title: '引导指南' },
},
{
path: '/withdraw',
name: 'withdraw',
component: () => import('@/views/Withdraw.vue'),
meta: { title: '提现', requiresAuth: true },
},
{
path: '/service',
name: 'service',
component: () => import('@/views/Service.vue'),
meta: { title: '客服' },
},
{
path: '/complaint',
name: 'complaint',
component: () => import('@/views/Complaint.vue'),
meta: { title: '投诉' },
},
{
path: '/report',
name: 'report',
component: () => import('@/views/Report.vue'),
path: "/historyQuery",
name: "history",
component: () => import("@/views/HistoryQuery.vue"),
meta: {
title: '报告结果', requiresAuth: true, notNeedBindPhone: true
title: "历史报告",
requiresAuth: true,
notNeedBindPhone: true,
},
},
{
path: '/example',
name: 'example',
component: () => import('@/views/Example.vue'),
meta: { title: '示例报告', notNeedBindPhone: true },
path: "/help",
name: "help",
component: () => import("@/views/Help.vue"),
meta: { title: "帮助中心" },
},
{
path: '/vant-theme-test',
name: 'vantThemeTest',
component: () => import('@/views/VantThemeTest.vue'),
meta: { title: 'Vant主题色测试' },
path: "/help/detail",
name: "helpDetail",
component: () => import("@/views/HelpDetail.vue"),
meta: { title: "帮助中心" },
},
{
path: '/privacyPolicy',
name: 'privacyPolicy',
component: () => import('@/views/PrivacyPolicy.vue'),
meta: { title: '隐私政策' },
path: "/help/guide",
name: "helpGuide",
component: () => import("@/views/HelpGuide.vue"),
meta: { title: "引导指南" },
},
{
path: '/userAgreement',
name: 'userAgreement',
component: () => import('@/views/UserAgreement.vue'),
meta: { title: '用户协议' },
path: "/withdraw",
name: "withdraw",
component: () => import("@/views/Withdraw.vue"),
meta: { title: "提现", requiresAuth: true },
},
{
path: '/agentManageAgreement',
name: 'agentManageAgreement',
component: () => import('@/views/AgentManageAgreement.vue'),
meta: { title: '代理管理协议' },
path: "/service",
name: "service",
component: () => import("@/views/Service.vue"),
meta: { title: "客服" },
},
{
path: '/agentSerivceAgreement',
name: 'agentSerivceAgreement',
component: () => import('@/views/AgentServiceAgreement.vue'),
meta: { title: '信息技术服务合同' },
path: "/complaint",
name: "complaint",
component: () => import("@/views/Complaint.vue"),
meta: { title: "投诉" },
},
{
path: '/inquire/:feature',
name: 'inquire',
component: () => import('@/views/Inquire.vue'),
meta: { title: '查询报告' },
path: "/report",
name: "report",
component: () => import("@/views/Report.vue"),
meta: {
title: "报告结果",
requiresAuth: true,
notNeedBindPhone: true,
},
},
{
path: '/authorization',
name: 'authorization',
component: () => import('@/views/Authorization.vue'),
meta: { title: '授权书' },
path: "/example",
name: "example",
component: () => import("@/views/Example.vue"),
meta: { title: "示例报告", notNeedBindPhone: true },
},
{
path: '/payment/result',
name: 'paymentResult',
component: () => import('@/views/PaymentResult.vue'),
meta: { title: '支付结果', requiresAuth: true },
path: "/vant-theme-test",
name: "vantThemeTest",
component: () =>
import("@/views/VantThemeTest.vue"),
meta: { title: "Vant主题色测试" },
},
{
path: "/privacyPolicy",
name: "privacyPolicy",
component: () =>
import("@/views/PrivacyPolicy.vue"),
meta: { title: "隐私政策" },
},
{
path: "/userAgreement",
name: "userAgreement",
component: () =>
import("@/views/UserAgreement.vue"),
meta: { title: "用户协议" },
},
{
path: "/agentManageAgreement",
name: "agentManageAgreement",
component: () =>
import("@/views/AgentManageAgreement.vue"),
meta: { title: "代理管理协议" },
},
{
path: "/agentSerivceAgreement",
name: "agentSerivceAgreement",
component: () =>
import("@/views/AgentServiceAgreement.vue"),
meta: { title: "信息技术服务合同" },
},
{
path: "/inquire/:feature",
name: "inquire",
component: () => import("@/views/Inquire.vue"),
meta: { title: "查询报告" },
},
{
path: "/authorization",
name: "authorization",
component: () =>
import("@/views/Authorization.vue"),
meta: { title: "授权书" },
},
{
path: "/payment/result",
name: "paymentResult",
component: () =>
import("@/views/PaymentResult.vue"),
meta: {
title: "支付结果",
requiresAuth: true,
notNeedBindPhone: true,
},
},
],
},
{
path: 'agent',
path: "agent",
component: PageLayout,
children: [
{
path: '/agent/promote',
name: 'agentPromote',
path: "/agent/promote",
name: "agentPromote",
component: Promote,
meta: {
title: '推广报告',
title: "推广报告",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'promoteDetails',
name: 'promoteDetails',
component: () => import('@/views/AgentPromoteDetails.vue'),
path: "/agent/promotion/query/list",
name: "agentPromotionQueryList",
component: () =>
import("@/views/AgentPromotionHistory.vue"),
meta: {
title: '直推报告收益明细',
title: "推广查询记录",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'rewardsDetails',
name: 'rewardsDetails',
component: () => import('@/views/AgentRewardsDetails.vue'),
path: "promoteDetails",
name: "promoteDetails",
component: () =>
import("@/views/AgentPromoteDetails.vue"),
meta: {
title: '代理奖励收益明细',
title: "我的推广收益",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "rewardsDetails",
name: "rewardsDetails",
component: () =>
import("@/views/AgentRewardsDetails.vue"),
meta: {
title: "下级推广收益",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'invitation',
name: 'invitation',
component: () => import('@/views/Invitation.vue'),
path: "invitation",
name: "invitation",
component: () =>
import("@/views/InvitationPage.vue"),
meta: {
title: '邀请下级',
title: "邀请下级",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'agentVip',
name: 'agentVip',
component: () => import('@/views/AgentVip.vue'),
path: "withdraw",
name: "withdraw",
component: () => import("@/views/Withdraw.vue"),
meta: {
title: '代理会员',
title: "提现",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'vipApply',
name: 'agentVipApply',
component: () => import('@/views/AgentVipApply.vue'),
path: "withdrawDetails",
name: "withdrawDetails",
component: () =>
import("@/views/WithdrawDetails.vue"),
meta: {
title: 'VIP代理申请',
title: "提现记录",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'vipConfig',
name: 'agentVipConfig',
component: () => import('@/views/AgentVipConfig.vue'),
path: "teamList",
name: "teamList",
component: () => import("@/views/TeamList.vue"),
meta: {
title: '代理会员报告配置',
title: "我的团队",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'withdraw',
name: 'withdraw',
component: () => import('@/views/Withdraw.vue'),
path: "upgradeSubordinate",
name: "upgradeSubordinate",
component: () =>
import("@/views/UpgradeSubordinate.vue"),
meta: {
title: '提现',
title: "调整下级级别",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'withdrawDetails',
name: 'withdrawDetails',
component: () => import('@/views/WithdrawDetails.vue'),
path: "upgrade",
name: "agentUpgrade",
component: () => import("@/views/AgentUpgrade.vue"),
meta: {
title: '提现记录',
title: "升级代理",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'invitationAgentApply/self',
name: 'invitationAgentApplySelf',
component: () => import('@/views/InvitationAgentApply.vue'),
meta: { title: '代理申请', requiresAuth: true },
},
{
path: 'subordinateList',
name: 'subordinateList',
component: () => import('@/views/SubordinateList.vue'),
path: "subordinateList",
name: "subordinateList",
component: () =>
import("@/views/SubordinateList.vue"),
meta: {
title: '我的下级',
title: "我的下级",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: 'subordinateDetail/:id',
name: 'subordinateDetail',
component: () => import('@/views/SubordinateDetail.vue'),
path: "subordinateDetail/:id",
name: "subordinateDetail",
component: () =>
import("@/views/SubordinateDetail.vue"),
meta: {
title: '下级贡献详情',
title: "下级贡献详情",
requiresAuth: true,
requiresAgent: true,
},
@@ -290,37 +320,42 @@ const router = createRouter({
],
},
{
path: 'app',
path: "app",
children: [
{
path: 'authorization',
name: 'appAuthorization',
component: () => import('@/views/Authorization.vue'),
meta: { title: '授权书' },
path: "authorization",
name: "appAuthorization",
component: () =>
import("@/views/Authorization.vue"),
meta: { title: "授权书" },
},
{
path: 'privacyPolicy',
name: 'appPrivacyPolicy',
component: () => import('@/views/PrivacyPolicy.vue'),
meta: { title: '隐私政策' },
path: "privacyPolicy",
name: "appPrivacyPolicy",
component: () =>
import("@/views/PrivacyPolicy.vue"),
meta: { title: "隐私政策" },
},
{
path: 'userAgreement',
name: 'appUserAgreement',
component: () => import('@/views/UserAgreement.vue'),
meta: { title: '用户协议' },
path: "userAgreement",
name: "appUserAgreement",
component: () =>
import("@/views/UserAgreement.vue"),
meta: { title: "用户协议" },
},
{
path: 'agentManageAgreement',
name: 'appAgentManageAgreement',
component: () => import('@/views/AgentManageAgreement.vue'),
meta: { title: '代理管理协议' },
path: "agentManageAgreement",
name: "appAgentManageAgreement",
component: () =>
import("@/views/AgentManageAgreement.vue"),
meta: { title: "代理管理协议" },
},
{
path: 'agentSerivceAgreement',
name: 'appAgentSerivceAgreement',
component: () => import('@/views/AgentServiceAgreement.vue'),
meta: { title: '信息技术服务合同' },
path: "agentSerivceAgreement",
name: "appAgentSerivceAgreement",
component: () =>
import("@/views/AgentServiceAgreement.vue"),
meta: { title: "信息技术服务合同" },
},
],
},
@@ -328,45 +363,50 @@ const router = createRouter({
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login.vue'),
path: "/login",
name: "login",
component: () => import("@/views/Login.vue"),
},
{
path: '/agent/promotionInquire/:linkIdentifier',
name: 'promotionInquire',
component: () => import('@/views/PromotionInquire.vue'),
path: "/register",
name: "register",
component: () => import("@/views/Register.vue"),
},
{
path: "/agent/promotionInquire/:linkIdentifier",
name: "promotionInquire",
component: () => import("@/views/PromotionInquire.vue"),
meta: { notNeedBindPhone: true },
},
{
path: '/agent/invitationAgentApply/:linkIdentifier',
name: 'invitationAgentApply',
component: () => import('@/views/InvitationAgentApply.vue'),
meta: { title: '代理申请' },
path: "/agent/invitationAgentApply/:linkIdentifier",
name: "invitationAgentApply",
component: () => import("@/views/InvitationAgentApply.vue"),
meta: { title: "代理申请" },
},
{
path: '/report/share/:linkIdentifier',
name: 'reportShare',
component: () => import('@/views/ReportShare.vue'),
path: "/report/share/:linkIdentifier",
name: "reportShare",
component: () => import("@/views/ReportShare.vue"),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("@/views/NotFound.vue"),
},
],
})
});
NProgress.configure({
easing: 'ease', // 动画方式
easing: "ease", // 动画方式
speed: 500, // 递增进度条的速度(毫秒)
showSpinner: false, // 是否显示加载的圆圈
trickleSpeed: 200, // 自动递增间隔
minimum: 0.3, // 初始化最小百分比
})
});
// 路由导航守卫
router.beforeEach(async (to, from, next) => {
NProgress.start(); // 启动进度条
NProgress.start();
const isAuthenticated = localStorage.getItem("token");
const agentStore = useAgentStore();
const userStore = useUserStore();
@@ -375,93 +415,89 @@ router.beforeEach(async (to, from, next) => {
const { isWeChat } = useEnv();
const { isAgent, isLoaded } = storeToRefs(agentStore);
const { mobile, isLoggedIn } = storeToRefs(userStore);
const { isWeixinAuthing, weixinAuthComplete } = storeToRefs(authStore);
// 微信环境下,如果正在进行授权,等待授权完成
if (isWeChat.value && isWeixinAuthing.value && !weixinAuthComplete.value) {
// 等待授权完成,使用响应式监听
await new Promise((resolve) => {
const stopWatcher = watch(
[isWeixinAuthing, weixinAuthComplete],
([authing, complete]) => {
if (!authing || complete) {
stopWatcher();
resolve();
}
},
{ immediate: true }
);
});
// 检查 token 是否过期
const accessExpire = localStorage.getItem("accessExpire");
const now = Date.now();
let isTokenExpired = false;
if (accessExpire) {
isTokenExpired = now > parseInt(accessExpire) * 1000;
}
// 处理需要登录的页面
if (to.meta.requiresAuth && !isAuthenticated) {
if (isWeChat.value) {
// 微信环境下,如果授权失败或超时,重定向到首页
if (!weixinAuthComplete.value) {
next("/");
location.reload();
} else {
// 授权完成但仍无token可能是授权失败
next("/");
location.reload();
}
} else {
next("/login");
// ============================================================
// 场景 2: 需要登录的页面 + 无 token 或 token 过期 → 跳转登录
// ============================================================
if (to.meta.requiresAuth && (!isAuthenticated || isTokenExpired)) {
const loginQuery = {
redirect: to.fullPath,
};
if (from && from.name === "promotionInquire") {
loginQuery.from = "promotionInquire";
}
next({ path: "/login", query: loginQuery });
return;
}
// 已登录状态下的处理
if (isAuthenticated) {
// ============================================================
// 场景 3: 已登录状态下的处理
// ============================================================
if (isAuthenticated && !isTokenExpired) {
// 确保用户信息已加载
if (!isLoggedIn.value) {
await userStore.fetchUserInfo();
}
// 检查手机号绑定状态
// 只有在未绑定手机号且目标路由需要登录并且没有设置notNeedBindPhone时才弹出绑定手机号弹窗
if (
!mobile.value &&
to.meta.requiresAuth &&
!to.meta.notNeedBindPhone
) {
dialogStore.openBindPhone();
next(false);
return;
}
// 检查代理权限
if (to.meta.requiresAgent) {
if (!isLoaded.value) {
await agentStore.fetchAgentStatus();
try {
await userStore.fetchUserInfo();
} catch (err) {
console.error("Error loading user info:", err);
}
}
// 检查代理权限(仅在 requiresAgent 为 true 时)
if (to.meta.requiresAgent) {
if (!mobile.value) {
if (to.meta.notNeedBindPhone) {
dialogStore.openBindPhone();
} else {
next("/register");
return;
}
}
// 确保代理信息已加载
if (!isLoaded.value) {
try {
await agentStore.fetchAgentStatus();
} catch (err) {
console.error("Error loading agent info:", err);
}
}
// 如果不是代理,跳转到注册页
if (!isAgent.value) {
next("/agent/invitationAgentApply/self");
next("/register");
return;
}
}
}
// ============================================================
// 其他情况正常通过
// ============================================================
next();
})
});
router.afterEach((to) => {
NProgress.done() // 结束进度条
NProgress.done(); // 结束进度条
// SEO优化更新页面标题和meta信息
const { updateSEO } = useSEO()
const { updateSEO } = useSEO();
// 根据路由meta信息更新SEO
if (to.meta.title) {
const seoConfig = {
title: `${to.meta.title} - 一查查`,
description: `一查查${to.meta.title}页面,提供专业的大数据风险管控服务。`,
url: `https://www.zhinengcha.cn${to.path}`
}
updateSEO(seoConfig)
url: `https://www.zhinengcha.cn${to.path}`,
};
updateSEO(seoConfig);
}
})
});
export default router
export default router;

View File

@@ -0,0 +1,58 @@
import useApiFetch from "@/composables/useApiFetch";
class AuthService {
detectPlatform() {
const ua = navigator.userAgent.toLowerCase();
if (/micromessenger/.test(ua)) return "wxh5";
return "h5";
}
async authenticate() {
const platform = this.detectPlatform();
return await this.authByPlatform(platform);
}
async authByPlatform(platform) {
switch (platform) {
case "h5":
return await this.authBrowser();
case "wxh5":
return await this.authWechatH5();
default:
return await this.authBrowser();
}
}
async authBrowser() {
let token = localStorage.getItem("token");
if (!token) {
const { data } = await useApiFetch("/user/auth")
.post({ platform: "h5" })
.json();
token = data.accessToken;
localStorage.setItem("token", token);
localStorage.setItem("refreshAfter", data.refreshAfter);
localStorage.setItem("accessExpire", data.accessExpire);
}
return token;
}
async authWechatH5() {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
if (code) {
const { data } = await useApiFetch("/user/auth")
.post({ platform: "wxh5", code })
.json();
window.history.replaceState({}, "", window.location.pathname);
localStorage.setItem("token", data.accessToken);
localStorage.setItem("refreshAfter", data.refreshAfter);
localStorage.setItem("accessExpire", data.accessExpire);
return data.accessToken;
} else {
return null;
}
}
}
export const authService = new AuthService();

View File

@@ -1,47 +1,97 @@
import { defineStore } from 'pinia'
import { getAgentInfo } from '@/api/agent'
export const useAgentStore = defineStore('agent', {
state: () => ({
isLoaded: false,
level: '',
status: 3, // 0=待审核1=审核通过2=审核未通过3=未申请
level: 0, // 1=普通2=黄金3=钻石
levelName: '', // 等级名称
isAgent: false,
ancestorID: null,
agentID: null,
agentCode: 0,
mobile: '',
ExpiryTime: '',
region: '',
wechatId: '',
teamLeaderId: null,
isRealName: false,
}),
getters: {
// 是否是代理
isAgentUser: (state) => state.isAgent && state.agentID !== null,
// 是否是钻石代理
isDiamond: (state) => state.level === 3,
// 是否是黄金代理
isGold: (state) => state.level === 2,
// 是否是普通代理
isNormal: (state) => state.level === 1,
// 获取等级显示名称
levelDisplayName: (state) => {
const levelMap = {
1: '普通代理',
2: '黄金代理',
3: '钻石代理'
}
return levelMap[state.level] || '普通代理'
}
},
actions: {
async fetchAgentStatus() {
const { data, error } = await useApiFetch('/agent/info').get().json()
const { data, error } = await getAgentInfo()
if (data.value && !error.value) {
if (data.value.code === 200) {
this.level = data.value.data.level
this.isAgent = data.value.data.is_agent // 判断是否是代理
this.status = data.value.data.status // 获取代理状态 0=待审核1=审核通过2=审核未通过3=未申请
this.agentID = data.value.data.agent_id
this.mobile = data.value.data.mobile
this.ExpiryTime = data.value.data.expiry_time
this.isRealName = data.value.data.is_real_name
const agentData = data.value.data
// 如果 agent_id 为 0说明不是代理
if (agentData.agent_id === 0) {
this.resetAgent()
} else {
this.level = agentData.level || 0
this.levelName = agentData.level_name || ''
this.isAgent = true // 如果能获取到信息,说明是代理
this.agentID = agentData.agent_id
this.agentCode = agentData.agent_code || 0
this.mobile = agentData.mobile || ''
this.region = agentData.region || ''
this.wechatId = agentData.wechat_id || ''
this.teamLeaderId = agentData.team_leader_id || null
this.isRealName = agentData.is_real_name || false
// 保存到localStorage
localStorage.setItem(
'agentInfo',
JSON.stringify({
isAgent: this.isAgent,
level: this.level,
status: this.status,
agentID: this.agentID,
mobile: this.mobile,
ExpiryTime: this.ExpiryTime,
isRealName: this.isRealName,
})
)
// 保存到localStorage
localStorage.setItem(
'agentInfo',
JSON.stringify({
isAgent: this.isAgent,
level: this.level,
levelName: this.levelName,
agentID: this.agentID,
agentCode: this.agentCode,
mobile: this.mobile,
region: this.region,
wechatId: this.wechatId,
teamLeaderId: this.teamLeaderId,
isRealName: this.isRealName,
})
)
}
} else {
console.log('Error fetching agent info', data.value)
// 检查是否是临时用户需要绑定手机号的错误100010
if (data.value.code === 100010) {
// 临时用户,不重置状态,静默处理
console.log('临时用户需要绑定手机号,跳过代理状态检查')
} else {
// 如果不是代理或获取失败,重置状态
this.resetAgent()
console.log('Error fetching agent info', data.value)
}
}
} else {
// 检查错误码
if (error.value && error.value.code === 100010) {
// 临时用户需要绑定手机号,静默处理
console.log('临时用户需要绑定手机号,跳过代理状态检查')
} else {
// 请求失败或未登录,重置状态
this.resetAgent()
}
}
this.isLoaded = true
@@ -51,25 +101,30 @@ export const useAgentStore = defineStore('agent', {
updateAgentInfo(agentInfo) {
if (agentInfo) {
this.isAgent = agentInfo.isAgent || false
this.level = agentInfo.level || ''
this.status = agentInfo.status || 3
this.level = agentInfo.level || 0
this.levelName = agentInfo.levelName || ''
this.agentID = agentInfo.agentID || null
this.mobile = agentInfo.mobile || ''
this.isLoaded = true
this.region = agentInfo.region || ''
this.wechatId = agentInfo.wechatId || ''
this.teamLeaderId = agentInfo.teamLeaderId || null
this.isRealName = agentInfo.isRealName || false
this.isLoaded = true
}
},
// 重置代理信息
resetAgent() {
this.isLoaded = false
this.level = ''
this.status = 3
this.level = 0
this.levelName = ''
this.isAgent = false
this.ancestorID = null
this.agentID = null
this.agentCode = 0
this.mobile = ''
this.region = ''
this.wechatId = ''
this.teamLeaderId = null
this.isRealName = false
},
},

21
src/stores/appStore.js Normal file
View File

@@ -0,0 +1,21 @@
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
queryRetentionDays: 0,
isLoaded: false,
}),
actions: {
async fetchAppConfig() {
try {
const { data, error } = await useApiFetch('/app/config').get().json()
if (data.value && !error.value && data.value.code === 200) {
const cfg = data.value.data
this.queryRetentionDays = Number(cfg.query_retention_days) || 0
}
} catch (e) {
}
this.isLoaded = true
},
},
})

View File

@@ -6,22 +6,51 @@ export const useAuthStore = defineStore("auth", {
isWeixinAuthing: false, // 是否正在进行微信授权
weixinAuthComplete: false, // 微信授权是否完成
pendingRoute: null, // 等待授权完成后跳转的路由
authStartTime: 0, // 授权开始时间,用于防止超时重复授权
}),
actions: {
// 开始微信授权
startWeixinAuth(targetRoute = null) {
// 如果已经在授权过程中,不再重复启动
if (this.isWeixinAuthing) {
console.warn("WeChat auth already in progress");
return;
}
this.isWeixinAuthing = true;
this.weixinAuthComplete = false;
this.pendingRoute = targetRoute;
this.authStartTime = Date.now();
// 保存到localStorage防止页面刷新后状态丢失
localStorage.setItem("weixinAuthing", "true");
localStorage.setItem(
"authStartTime",
this.authStartTime.toString()
);
if (targetRoute) {
localStorage.setItem(
"pendingRoute",
JSON.stringify(targetRoute)
);
const routeData =
typeof targetRoute === "string"
? targetRoute
: targetRoute.fullPath || targetRoute.path || "";
if (routeData) {
let sanitized = routeData;
const parts = routeData.split("?");
if (parts.length > 1) {
const base = parts[0];
const params = new URLSearchParams(parts[1]);
params.delete("code");
params.delete("state");
const qs = params.toString();
sanitized = qs ? `${base}?${qs}` : base;
}
localStorage.setItem(
"pendingRoute",
JSON.stringify(sanitized)
);
}
}
},
@@ -29,16 +58,11 @@ export const useAuthStore = defineStore("auth", {
completeWeixinAuth() {
this.isWeixinAuthing = false;
this.weixinAuthComplete = true;
this.authStartTime = 0;
// 清除localStorage中的授权状态
localStorage.removeItem("weixinAuthing");
localStorage.removeItem("pendingRoute");
},
// 清除待处理路由
clearPendingRoute() {
this.pendingRoute = null;
localStorage.removeItem("pendingRoute");
localStorage.removeItem("authStartTime");
},
// 重置授权状态
@@ -46,22 +70,47 @@ export const useAuthStore = defineStore("auth", {
this.isWeixinAuthing = false;
this.weixinAuthComplete = false;
this.pendingRoute = null;
this.authStartTime = 0;
localStorage.removeItem("weixinAuthing");
localStorage.removeItem("pendingRoute");
localStorage.removeItem("authStartTime");
},
// 检查授权是否超时超过30秒视为超时
isAuthTimeout() {
if (!this.authStartTime) return false;
const elapsed = Date.now() - this.authStartTime;
return elapsed > 30000; // 30秒超时
},
// 从localStorage恢复状态页面刷新后调用
restoreFromStorage() {
const isAuthing = localStorage.getItem("weixinAuthing") === "true";
const pendingRouteStr = localStorage.getItem("pendingRoute");
const authStartTime = localStorage.getItem("authStartTime");
if (isAuthing) {
console.log("🔄 Restoring WeChat auth state from storage");
this.isWeixinAuthing = true;
this.weixinAuthComplete = false;
this.authStartTime = authStartTime
? parseInt(authStartTime)
: 0;
// 检查是否超时,如果超时则重置
if (this.isAuthTimeout()) {
console.warn("WeChat auth timeout, resetting state");
this.resetAuthState();
return;
}
if (pendingRouteStr) {
try {
this.pendingRoute = JSON.parse(pendingRouteStr);
console.log(
"✅ Restored pendingRoute from storage:",
this.pendingRoute
);
} catch (e) {
console.error("Failed to parse pending route:", e);
this.pendingRoute = null;
@@ -69,5 +118,12 @@ export const useAuthStore = defineStore("auth", {
}
}
},
// 清除待处理路由时确保同步清除内存和localStorage
clearPendingRoute() {
this.pendingRoute = null;
localStorage.removeItem("pendingRoute");
console.log("✅ Cleared pendingRoute from both memory and storage");
},
},
});

View File

@@ -2,7 +2,8 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useDialogStore = defineStore('dialog', () => {
const showBindPhone = ref(false)
const showBindPhone = ref(false) // 推广页面专用的绑定手机号(不要求邀请码)
const showRegisterAgent = ref(false) // 注册成为代理(带邀请码)
const showRealNameAuth = ref(false)
function openBindPhone() {
@@ -13,6 +14,14 @@ export const useDialogStore = defineStore('dialog', () => {
showBindPhone.value = false
}
function openRegisterAgent() {
showRegisterAgent.value = true
}
function closeRegisterAgent() {
showRegisterAgent.value = false
}
function openRealNameAuth() {
showRealNameAuth.value = true
}
@@ -25,6 +34,9 @@ export const useDialogStore = defineStore('dialog', () => {
showBindPhone,
openBindPhone,
closeBindPhone,
showRegisterAgent,
openRegisterAgent,
closeRegisterAgent,
showRealNameAuth,
openRealNameAuth,
closeRealNameAuth,

View File

@@ -1,59 +1,76 @@
import { defineStore } from 'pinia'
import { defineStore } from "pinia";
export const useUserStore = defineStore('user', {
state: () => ({
userName: '',
mobile: '',
userAvatar: '',
isLoggedIn: false,
}),
actions: {
async fetchUserInfo() {
const { data, error } = await useApiFetch('/user/detail').get().json()
if (data.value && !error.value) {
if (data.value.code === 200) {
const userinfo = data.value.data.userInfo
this.userName = userinfo.mobile || ''
this.mobile = userinfo.mobile || ''
this.userAvatar = userinfo.userAvatar
this.isLoggedIn = true
export const useUserStore = defineStore("user", {
state: () => ({
userName: "",
mobile: "",
userAvatar: "",
isLoggedIn: false,
}),
actions: {
async fetchUserInfo() {
const { data, error } = await useApiFetch("/user/detail")
.get()
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
const userinfo = data.value.data.userInfo;
this.userName = userinfo.mobile || "";
this.mobile = userinfo.mobile || "";
this.userAvatar = userinfo.userAvatar;
this.isLoggedIn = true;
// 保存到localStorage
localStorage.setItem(
'userInfo',
JSON.stringify({
nickName: this.userName,
avatar: this.userAvatar,
})
)
} else if (data.value.code === 100009) {
localStorage.removeItem('token')
localStorage.removeItem('refreshAfter')
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
// 保存到localStorage
localStorage.setItem(
"userInfo",
JSON.stringify({
nickName: this.userName,
avatar: this.userAvatar,
})
);
} else if (data.value.code === 100009) {
// Token 无效或用户不存在,清除数据但不 reload
// reload 会导致无限循环
console.warn(
"User not found or token invalid (100009), clearing auth data"
);
localStorage.removeItem("token");
localStorage.removeItem("refreshAfter");
localStorage.removeItem("accessExpire");
localStorage.removeItem("userInfo");
localStorage.removeItem("agentInfo");
this.resetUser()
window.location.reload()
}
} else {
}
this.resetUser();
// 不要 reload让调用者处理错误
throw new Error("User not found or token invalid");
} else {
// 其他错误
console.error("Unexpected response code:", data.value.code);
throw new Error(
`Unexpected response code: ${data.value.code}`
);
}
} else {
console.error("API error:", error.value);
throw error.value || new Error("Unknown error");
}
},
// 更新用户信息
updateUserInfo(userInfo) {
if (userInfo) {
this.userName = userInfo.mobile || userInfo.nickName || "";
this.userAvatar = userInfo.avatar || "";
this.isLoggedIn = true;
}
},
// 重置用户信息
resetUser() {
this.userName = "";
this.userAvatar = "";
this.isLoggedIn = false;
},
},
// 更新用户信息
updateUserInfo(userInfo) {
if (userInfo) {
this.userName = userInfo.mobile || userInfo.nickName || ''
this.userAvatar = userInfo.avatar || ''
this.isLoggedIn = true
}
},
// 重置用户信息
resetUser() {
this.userName = ''
this.userAvatar = ''
this.isLoggedIn = false
},
},
})
});

View File

@@ -1,292 +1,360 @@
<template>
<div class="p-4 bg-gradient-to-b from-gray-50/50 to-gray-100/30 min-h-screen">
<!-- 资产卡片 -->
<div class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-primary-50/70 to-primary-100/50 p-6">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center">
<van-icon name="balance-pay" class="text-xl mr-2" style="color: var(--van-theme-primary);" />
<span class="text-lg font-bold" style="color: var(--van-text-color);">余额</span>
</div>
<span class="text-3xl font-bold" style="color: var(--van-theme-primary);">¥ {{ (data?.balance ||
0).toFixed(2) }}</span>
</div>
<div class="text-sm mb-2" style="color: var(--van-text-color-2);">
累计收益¥ {{ (data?.total_earnings || 0).toFixed(2) }}
</div>
<div class="text-sm mb-6" style="color: var(--van-text-color-2);">
冻结余额¥ {{ (data?.frozen_balance || 0).toFixed(2) }}
</div>
<div class="grid grid-cols-2 gap-3">
<button @click="toWithdraw"
class="text-white rounded-full py-2 px-4 shadow-md flex items-center justify-center"
style="background: linear-gradient(135deg, var(--van-theme-primary), var(--van-theme-primary-dark));">
<van-icon name="gold-coin" class="mr-1" />
提现
</button>
<button @click="toWithdrawDetails"
class="bg-white/90 border rounded-full py-2 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="notes" class="mr-1" />
提现记录
</button>
</div>
</div>
<!-- 直推报告收益 -->
<div class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-warning-50/60 to-warning-100/50 p-6">
<!-- 团队统计 -->
<div class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-purple-50/60 to-purple-100/50 p-6">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<van-icon name="balance-list" class="text-xl mr-2" style="color: var(--color-warning);" />
<span class="text-lg font-bold" style="color: var(--van-text-color);">直推报告收益</span>
<van-icon name="friends-o" class="text-xl mr-2" style="color: #8b5cf6;" />
<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-warning);">
¥
{{
(data?.direct_push?.total_commission || 0).toFixed(
2
)
}}
</div>
<div class="text-sm mt-1" style="color: var(--van-text-color-2);">
有效报告 {{ data?.direct_push?.total_report || 0 }}
</div>
</div>
<!-- 第二行总数/今日新增/本月新增 -->
<div class="grid grid-cols-3 gap-3 mb-4">
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="text-lg font-bold" style="color: #8b5cf6;">{{ teamStats?.total_count || 0 }}</div>
<div class="text-sm mt-1" style="color: var(--van-theme-primary);">团队人数</div>
</div>
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="text-lg font-bold" style="color: #8b5cf6;">{{ teamStats?.today_new_members || 0 }}</div>
<div class="text-sm mt-1" style="color: var(--van-theme-primary);">今日新增</div>
</div>
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="text-lg font-bold" style="color: #8b5cf6;">{{ teamStats?.month_new_members || 0 }}</div>
<div class="text-sm mt-1" style="color: var(--van-theme-primary);">本月新增</div>
</div>
</div>
<!-- 第一行直接/间接黄金/普通 -->
<div class="grid grid-cols-4 gap-3 mb-4">
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="text-lg font-bold" style="color: #8b5cf6;">{{ teamStats?.direct_count || 0 }}</div>
<div class="text-sm mt-1" style="color: var(--van-theme-primary);">直接下级</div>
</div>
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="text-lg font-bold" style="color: #8b5cf6;">{{ teamStats?.indirect_count || 0 }}</div>
<div class="text-sm mt-1" style="color: var(--van-theme-primary);">间接下级</div>
</div>
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="text-lg font-bold" style="color: #f59e0b;">{{ teamStats?.gold_count || 0 }}</div>
<div class="text-sm mt-1" style="color: var(--van-theme-primary);">黄金下级</div>
</div>
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="text-lg font-bold" style="color: #6b7280;">{{ teamStats?.normal_count || 0 }}</div>
<div class="text-sm mt-1" style="color: var(--van-theme-primary);">普通下级</div>
</div>
</div>
<!-- 日期选择 -->
<div class="grid grid-cols-3 gap-2 mb-6">
<button v-for="item in promoteDateOptions" :key="item.value" @click="selectedPromoteDate = item.value"
class="rounded-full transition-all py-1 px-4 text-sm" :class="[
selectedPromoteDate === item.value
? 'text-white shadow-md'
: 'bg-white/90 border',
]" :style="selectedPromoteDate === item.value
? 'background-color: var(--color-warning);'
: 'color: var(--van-text-color-2); border-color: var(--van-border-color);'">
{{ item.label }}
</button>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="p-3 rounded-lg backdrop-blur-sm" style="background-color: rgba(245, 158, 11, 0.08);">
<div class="flex items-center text-sm" style="color: var(--van-text-color-2);">
<van-icon name="gold-coin" class="mr-1" />本日收益
</div>
<div class="text-xl font-bold mt-1" style="color: var(--color-warning);">
¥
{{
currentPromoteData.commission?.toFixed(2) || "0.00"
}}
</div>
</div>
<div class="p-3 rounded-lg backdrop-blur-sm" style="background-color: rgba(245, 158, 11, 0.08);">
<div class="flex items-center text-sm" style="color: var(--van-text-color-2);">
<van-icon name="description" class="mr-1" />有效报告
</div>
<div class="text-xl font-bold mt-1" style="color: var(--color-warning);">
{{ currentPromoteData.report || 0 }}
</div>
</div>
</div>
<div class="flex items-center justify-between text-sm font-semibold cursor-pointer pt-4"
style="color: var(--color-warning);" @click="goToPromoteDetail">
<span>查看收益明细</span>
<span class="text-lg"></span>
</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 }}
</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="contact" class="mr-1" />新增活跃奖励
</div>
<div class="text-xl font-bold mt-1" style="color: var(--color-success);">
¥
{{
(currentActiveData.sub_upgrade_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));">
<button @click="toTeamList"
class="w-full text-white rounded-full py-2 px-4 shadow-md flex items-center justify-center"
style="background: linear-gradient(135deg, #8b5cf6, #7c3aed);">
<van-icon name="friends" class="mr-1" />
查看我的下级
查看我的团队
</button>
</div>
</div>
<!-- 我的转化率 -->
<div class="relative rounded-xl shadow-lg mb-3 bg-white overflow-hidden">
<!-- 标题和Tab区域 -->
<div class="relative p-5 pb-0">
<div class="flex items-center mb-4">
<van-icon name="chart-trending-o" class="text-lg mr-2" style="color: var(--van-theme-primary);" />
<span class="text-lg font-bold" style="color: var(--van-text-color);">我的转化率</span>
</div>
<!-- 自定义 Tab与title对齐 -->
<div class="absolute top-3 right-3 flex p-1 rounded-2xl"
style="background-color: var(--van-theme-primary);">
<button @click="myConversionActiveTab = 'daily'" :class="[
'px-6 py-2 text-sm font-medium rounded-lg mx-1',
myConversionActiveTab === 'daily'
? 'bg-white text-gray-800'
: 'text-white'
]">
</button>
<button @click="myConversionActiveTab = 'weekly'" :class="[
'px-6 py-2 text-sm font-medium rounded-lg mx-1',
myConversionActiveTab === 'weekly'
? 'bg-white text-gray-800'
: 'text-white'
]">
</button>
<button @click="myConversionActiveTab = 'monthly'" :class="[
'px-6 py-2 text-sm font-medium rounded-lg mx-1',
myConversionActiveTab === 'monthly'
? 'bg-white text-gray-800'
: 'text-white'
]">
</button>
</div>
</div>
<!-- 内容区域 -->
<div class="p-5 pt-0">
<div v-if="!isLoggedIn"
class="p-4 rounded-lg bg-gradient-to-r from-gray-50 to-white border border-gray-200 text-center">
<div class="text-sm mb-3" style="color: var(--van-text-color);">请先登录后查看数据</div>
<button @click="toLogin" class="px-4 py-2 rounded-full text-white"
style="background: linear-gradient(135deg, #8b5cf6, #7c3aed);">
去登录
</button>
</div>
<template v-else>
<template v-if="isLoadingConversion">
<div class="p-3">
<van-skeleton :title="false" :row="3" />
</div>
</template>
<template v-else>
<div v-if="myConversionActiveTab === 'daily'" class="">
<div v-for="item in conversionData?.my_conversion_rate?.daily || []"
:key="item.period_label" class="p-4 flex items-center border-b border-gray-200 ">
<div class="text-lg font-semibold mr-4" style="color: var(--van-text-color);">
{{ item.period_label }}
</div>
<div class="flex items-center gap-6 text-base">
<div style="color: var(--van-theme-primary);">
{{ item.query_user_count || 0 }}人查询
</div>
<div style="color: var(--van-theme-primary);">
{{ item.paid_user_count || 0 }}人付费
</div>
<div style="color: var(--van-theme-primary);">
总金额: {{ (item.total_amount || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
<div v-if="myConversionActiveTab === 'weekly'" class="">
<div v-for="item in conversionData?.my_conversion_rate?.weekly || []"
:key="item.period_label" class="p-4 flex items-center border-b border-gray-200 ">
<div class="text-lg font-semibold mr-4" style="color: var(--van-text-color);">
{{ item.period_label }}
</div>
<div class="flex items-center gap-6 text-base">
<div style="color: var(--van-theme-primary);">
{{ item.query_user_count || 0 }}人查询
</div>
<div style="color: var(--van-theme-primary);">
{{ item.paid_user_count || 0 }}人付费
</div>
<div style="color: var(--van-theme-primary);">
总金额: {{ (item.total_amount || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
<div v-if="myConversionActiveTab === 'monthly'" class="">
<div v-for="item in conversionData?.my_conversion_rate?.monthly || []"
:key="item.period_label" class="p-4 flex items-center border-b border-gray-200 ">
<div class="text-lg font-semibold mr-4" style="color: var(--van-text-color);">
{{ item.period_label }}
</div>
<div class="flex items-center gap-6 text-base">
<div style="color: var(--van-theme-primary);">
{{ item.query_user_count || 0 }}人查询
</div>
<div style="color: var(--van-theme-primary);">
{{ item.paid_user_count || 0 }}人付费
</div>
<div style="color: var(--van-theme-primary);">
总金额: {{ (item.total_amount || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
<!-- 我的下级转化率 -->
<div class="relative rounded-xl shadow-lg mb-3 bg-white overflow-hidden">
<!-- 标题和Tab区域 -->
<div class="relative p-5 pb-0">
<div class="flex items-center mb-4">
<van-icon name="bar-chart-o" class="text-lg mr-2" style="color: var(--color-success);" />
<span class="text-lg font-bold" style="color: var(--van-text-color);">我的下级转化率</span>
</div>
<!-- 自定义 Tab与title对齐 -->
<div class="absolute top-3 right-3 flex p-1 rounded-2xl"
style="background-color: var(--van-theme-primary);">
<button @click="subordinateConversionActiveTab = 'daily'" :class="[
'px-6 py-2 text-sm font-medium rounded-lg mx-1',
subordinateConversionActiveTab === 'daily'
? 'bg-white text-gray-800'
: 'text-white'
]">
</button>
<button @click="subordinateConversionActiveTab = 'weekly'" :class="[
'px-6 py-2 text-sm font-medium rounded-lg mx-1',
subordinateConversionActiveTab === 'weekly'
? 'bg-white text-gray-800'
: 'text-white'
]">
</button>
<button @click="subordinateConversionActiveTab = 'monthly'" :class="[
'px-6 py-2 text-sm font-medium rounded-lg mx-1',
subordinateConversionActiveTab === 'monthly'
? 'bg-white text-gray-800'
: 'text-white'
]">
</button>
</div>
</div>
<!-- 内容区域 -->
<div class="p-5 pt-0">
<div v-if="!isLoggedIn"
class="p-4 rounded-lg bg-gradient-to-r from-gray-50 to-white border border-gray-200 text-center">
<div class="text-sm mb-3" style="color: var(--van-text-color);">请先登录后查看数据</div>
<button @click="toLogin" class="px-4 py-2 rounded-full text-white"
style="background: linear-gradient(135deg, #8b5cf6, #7c3aed);">
去登录
</button>
</div>
<template v-else>
<template v-if="isLoadingConversion">
<div class="p-3">
<van-skeleton :title="false" :row="3" />
</div>
</template>
<template v-else>
<div v-if="subordinateConversionActiveTab === 'daily'" class="">
<div v-for="item in conversionData?.subordinate_conversion_rate?.daily || []"
:key="item.period_label" class="p-4 flex items-center border-b border-gray-200 ">
<div class="text-lg font-semibold mr-4" style="color: var(--van-text-color);">
{{ item.period_label }}
</div>
<div class="flex items-center gap-6 text-base">
<div style="color: var(--van-theme-primary);">
{{ item.query_user_count || 0 }}人查询
</div>
<div style="color: var(--van-theme-primary);">
{{ item.paid_user_count || 0 }}人付费
</div>
<div style="color: var(--van-theme-primary);">
总金额: {{ (item.total_amount || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
<div v-if="subordinateConversionActiveTab === 'weekly'" class="">
<div v-for="item in conversionData?.subordinate_conversion_rate?.weekly || []"
:key="item.period_label" class="p-4 flex items-center border-b border-gray-200 ">
<div class="text-lg font-semibold mr-4" style="color: var(--van-text-color);">
{{ item.period_label }}
</div>
<div class="flex items-center gap-6 text-base">
<div style="color: var(--van-theme-primary);">
{{ item.query_user_count || 0 }}人查询
</div>
<div style="color: var(--van-theme-primary);">
{{ item.paid_user_count || 0 }}人付费
</div>
<div style="color: var(--van-theme-primary);">
总金额: {{ (item.total_amount || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
<div v-if="subordinateConversionActiveTab === 'monthly'" class="">
<div v-for="item in conversionData?.subordinate_conversion_rate?.monthly || []"
:key="item.period_label" class="p-4 flex items-center border-b border-gray-200 ">
<div class="text-lg font-semibold mr-4" style="color: var(--van-text-color);">
{{ item.period_label }}
</div>
<div class="flex items-center gap-6 text-base">
<div style="color: var(--van-theme-primary);">
{{ item.query_user_count || 0 }}人查询
</div>
<div style="color: var(--van-theme-primary);">
{{ item.paid_user_count || 0 }}人付费
</div>
<div style="color: var(--van-theme-primary);">
总金额: {{ (item.total_amount || 0).toFixed(2) }}
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { ref, computed } from "vue";
import { ref, computed, onMounted, watch } from "vue";
import { useRouter } from "vue-router";
import { getTeamStatistics, getConversionRate } from '@/api/agent'
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
const agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore);
const userStore = useUserStore();
const { isLoggedIn } = storeToRefs(userStore);
const router = useRouter();
const data = ref(null);
// 日期选项映射
const dateRangeMap = {
today: "today",
week: "last7d",
month: "last30d",
};
// 直推报告数据
const promoteDateOptions = [
{ label: "今日", value: "today" },
{ label: "近7天", value: "week" },
{ label: "近1月", value: "month" },
];
const selectedPromoteDate = ref("today");
// 活跃下级数据
const activeDateOptions = [
{ label: "今日", value: "today" },
{ label: "近7天", value: "week" },
{ label: "近1月", value: "month" },
];
const selectedActiveDate = ref("today");
// 计算当前直推数据
const currentPromoteData = computed(() => {
const range = dateRangeMap[selectedPromoteDate.value];
return data.value?.direct_push?.[range] || { commission: 0, report: 0 };
});
// 计算当前活跃数据
const currentActiveData = computed(() => {
const range = dateRangeMap[selectedActiveDate.value];
return (
data.value?.active_reward?.[range] || {
active_reward: 0,
sub_promote_reward: 0,
sub_upgrade_reward: 0,
sub_withdraw_reward: 0,
}
);
});
const teamStats = ref(null);
const conversionData = ref(null);
const myConversionActiveTab = ref('daily');
const subordinateConversionActiveTab = ref('daily');
const isLoadingConversion = ref(true);
const hasLoaded = ref(false);
const getData = async () => {
const { data: res, error } = await useApiFetch("/agent/revenue")
.get()
.json();
if (res.value?.code === 200 && !error.value) {
data.value = res.value.data;
// 获取团队统计
const { data: teamData, error: teamError } = await getTeamStatistics();
if (teamData.value?.code === 200 && !teamError.value) {
teamStats.value = teamData.value.data;
}
// 获取转化率统计
const { data: conversionRateData, error: conversionError } = await getConversionRate();
if (conversionRateData.value?.code === 200 && !conversionError.value) {
conversionData.value = conversionRateData.value.data;
}
isLoadingConversion.value = false;
};
onMounted(() => {
if (isAgent.value) {
if (isAgent.value && !hasLoaded.value) {
isLoadingConversion.value = true;
hasLoaded.value = true;
getData();
}
if (!isAgent.value) {
isLoadingConversion.value = false;
}
});
watch(isAgent, (val) => {
if (val && !hasLoaded.value) {
isLoadingConversion.value = true;
hasLoaded.value = true;
getData();
}
});
// 路由跳转
const goToPromoteDetail = () => router.push({ name: "promoteDetails" });
const goToActiveDetail = () => router.push({ name: "rewardsDetails" });
// 添加跳转到团队列表的方法
const toTeamList = () => {
router.push("/agent/teamList");
};
const toWithdraw = () => router.push({ name: "withdraw" });
const toPromotionQueryList = () => {
router.push({ name: "agentPromotionQueryList" });
};
const toWithdrawDetails = () => router.push({ name: "withdrawDetails" });
// 添加跳转到下级列表的方法
const toSubordinateList = () => {
router.push("/agent/subordinateList");
const toLogin = () => {
router.push("/login");
};
</script>
<style>
/* 添加按钮悬停效果 */
button {
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-1px);
}
</style>
<style></style>

View File

@@ -7,19 +7,24 @@
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
<span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span>
</div>
<div class="flex items-center">
<div class="flex items-center justify-between mb-2">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.product_name)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.product_name)"></span>
{{ item.product_name }}
</span>
</div>
<div v-if="item.order_no" class="text-xs text-gray-400">
订单号{{ item.order_no }}
</div>
</div>
</van-list>
</div>
</template>
<script setup>
import { getCommissionList } from '@/api/agent'
// 颜色配置(根据产品名称映射)
const typeColors = {
'小微企业': { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' },
@@ -28,7 +33,7 @@ const typeColors = {
'婚恋风险': { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' },
'贷前风险': { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' },
'消金报告': { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' },
'个人大数据': { bg: 'bg-red-100', text: 'text-red-800', dot: 'bg-red-500' },
'个人风险': { bg: 'bg-red-100', text: 'text-red-800', dot: 'bg-red-500' },
// 默认类型
'default': { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' }
}
@@ -63,11 +68,14 @@ const onLoad = async () => {
// 获取数据
const getData = async () => {
if (loading.value || finished.value) return
try {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/commission?page=${page.value}&page_size=${pageSize.value}`
).get().json()
const { data: res, error } = await getCommissionList({
page: page.value,
page_size: pageSize.value
})
if (res.value?.code === 200 && !error.value) {
// 首次加载
@@ -83,7 +91,15 @@ const getData = async () => {
res.value.data.list.length < pageSize.value) {
finished.value = true
}
} else {
// 接口返回错误或请求失败,停止翻页
finished.value = true
console.error('获取佣金列表失败:', res.value?.msg || error.value || '未知错误')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取佣金列表失败:', err)
} finally {
loading.value = false
}

View File

@@ -0,0 +1,113 @@
<script setup>
import { ref, onMounted } from 'vue'
const router = useRouter()
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const reportList = ref([])
const loading = ref(false)
const finished = ref(false)
async function fetchData() {
if (loading.value || finished.value) return
loading.value = true
try {
const { data, error } = await useApiFetch(`/agent/promotion/query/list?page=${page.value}&page_size=${pageSize.value}`).get().json()
if (data.value && !error.value) {
if (data.value.code === 200) {
total.value = data.value.data.total
if (data.value.data.list && data.value.data.list.length > 0) {
reportList.value.push(...data.value.data.list)
page.value += 1
}
if (reportList.value.length >= total.value) {
finished.value = true
}
} else {
finished.value = true
console.error('获取推广查询列表失败:', data.value.msg || '未知错误')
}
} else {
finished.value = true
console.error('获取推广查询列表失败:', error.value || '请求失败')
}
} catch (err) {
finished.value = true
console.error('获取推广查询列表失败:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
const onLoad = () => {
if (!finished.value) {
fetchData()
}
}
function toDetail(item) {
if (item.query_state != 'success') return
router.push({ path: '/report', query: { orderId: item.order_id } })
}
function stateText(state) {
switch (state) {
case 'pending':
return '查询中'
case 'success':
return '查询成功'
case 'failed':
return '查询失败'
case 'refunded':
return '已退款'
default:
return '未知状态'
}
}
function statusClass(state) {
switch (state) {
case 'pending':
return 'status-pending'
case 'success':
return 'status-success'
case 'failed':
return 'status-failed'
case 'refunded':
return 'status-refunded'
default:
return ''
}
}
</script>
<template>
<div class="flex flex-col gap-4 p-4">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="item in reportList" :key="item.id" @click="toDetail(item)"
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">
<div class="flex flex-col">
<div class="text-xl text-black mb-1">{{ item.product_name }}</div>
<div class="text-sm text-[#999999]">{{ item.create_time }}</div>
</div>
<div class="absolute top-0 right-0 rounded-bl-lg rounded-tr-lg px-2 py-[1px] text-white text-sm font-medium"
:class="[statusClass(item.query_state)]">
{{ stateText(item.query_state) }}
</div>
</div>
</van-list>
</div>
<van-empty v-if="!loading && reportList.length === 0" description="暂无推广查询记录" />
</template>
<style scoped>
.status-pending { background-color: #1976d2; color: white; }
.status-success { background-color: #1FBE5D; color: white; }
.status-failed { background-color: #EB3C3C; color: white; }
.status-refunded { background-color: #999999; color: white; }
</style>

View File

@@ -1,5 +1,11 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- Tab 标签页 -->
<van-tabs v-model:active="activeTab" @change="onTabChange" class="bg-white sticky top-0 z-10">
<van-tab title="推广返佣" name="promote"></van-tab>
<van-tab title="升级返佣" name="upgrade"></van-tab>
</van-tabs>
<!-- 收益列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
@@ -7,12 +13,35 @@
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
<span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span>
</div>
<div class="flex items-center">
<!-- 推广返佣显示返佣类型 tag -->
<!-- <div class="flex items-center mb-2" v-if="activeTab === 'promote' && item.rebate_type">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.type)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.type)"></span>
{{ typeToChinese(item.type) }}
:class="getReportTypeStyle(item.rebate_type)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.rebate_type)"></span>
{{ typeToChinese(item.rebate_type) }}
</span>
</div> -->
<!-- 升级返佣显示升级信息 tag -->
<div class="flex items-center mb-2" v-if="activeTab === 'upgrade' && item.to_level">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getUpgradeTagStyle(item.to_level)">
<span class="w-2 h-2 rounded-full mr-1" :class="getUpgradeTagDot(item.to_level)"></span>
升级为{{ getLevelName(item.to_level) }}代理
</span>
</div>
<div class="text-xs text-gray-400 space-y-1">
<div v-if="item.order_no">
订单号{{ item.order_no }}
</div>
<div v-if="item.source_agent_mobile" class="flex items-center gap-2">
<span>来源代理{{ item.source_agent_mobile }}</span>
<!-- 只在升级返佣 tab 显示来源代理等级 -->
<span v-if="activeTab === 'upgrade' && item.source_agent_level"
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
:class="getLevelTagStyle(item.source_agent_level)">
{{ getLevelName(item.source_agent_level) }}
</span>
</div>
</div>
</div>
</van-list>
@@ -20,38 +49,56 @@
</template>
<script setup>
// 类型映射配置
import { ref, onMounted } from 'vue'
import { getRebateList, getUpgradeRebateList } from '@/api/agent'
// 返佣类型映射配置(推广返佣)
const typeConfig = {
descendant_promotion: {
chinese: '下级推广奖励',
1: {
chinese: '直接上级返佣',
color: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' }
},
descendant_upgrade_vip: {
chinese: '下级升级VIP奖励',
color: { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' }
},
descendant_upgrade_svip: {
chinese: '下级升级SVIP奖励',
2: {
chinese: '钻石上级返佣',
color: { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' }
},
descendant_stay_activedescendant: {
chinese: '下级活跃奖励',
color: { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' }
},
new_active: {
chinese: '新增活跃奖励',
color: { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' }
},
descendant_withdraw: {
chinese: '下级提现奖励',
color: { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' }
3: {
chinese: '黄金上级返佣',
color: { bg: 'bg-yellow-100', text: 'text-yellow-800', dot: 'bg-yellow-500' }
},
default: {
chinese: '其他奖励',
chinese: '其他返佣',
color: { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' }
}
}
// 等级 tag 样式配置
const levelTagConfig = {
1: {
chinese: '普通',
color: { bg: 'bg-gray-100', text: 'text-gray-700' }
},
2: {
chinese: '黄金',
color: { bg: 'bg-yellow-100', text: 'text-yellow-700' }
},
3: {
chinese: '钻石',
color: { bg: 'bg-purple-100', text: 'text-purple-700' }
}
}
// 升级 tag 样式配置
const upgradeTagConfig = {
2: {
color: { bg: 'bg-yellow-100', text: 'text-yellow-700', dot: 'bg-yellow-500' }
},
3: {
color: { bg: 'bg-purple-100', text: 'text-purple-700', dot: 'bg-purple-500' }
}
}
const activeTab = ref('promote') // 'promote' 推广返佣, 'upgrade' 升级返佣
const page = ref(1)
const pageSize = ref(10)
const data = ref({
@@ -62,19 +109,55 @@ const loading = ref(false)
const finished = ref(false)
// 类型转中文
const typeToChinese = (type) => {
return typeConfig[type]?.chinese || typeConfig.default.chinese
const typeToChinese = (rebateType) => {
return typeConfig[rebateType]?.chinese || typeConfig.default.chinese
}
// 获取颜色样式
const getReportTypeStyle = (type) => {
const config = typeConfig[type] || typeConfig.default
const getReportTypeStyle = (rebateType) => {
const config = typeConfig[rebateType] || typeConfig.default
return `${config.color.bg} ${config.color.text}`
}
// 获取小圆点颜色
const getDotColor = (type) => {
return typeConfig[type]?.color.dot || typeConfig.default.color.dot
const getDotColor = (rebateType) => {
return typeConfig[rebateType]?.color.dot || typeConfig.default.color.dot
}
// 获取等级名称
const getLevelName = (level) => {
return levelTagConfig[level]?.chinese || '未知'
}
// 获取等级 tag 样式
const getLevelTagStyle = (level) => {
const config = levelTagConfig[level] || levelTagConfig[1]
return `${config.color.bg} ${config.color.text}`
}
// 获取升级 tag 样式
const getUpgradeTagStyle = (toLevel) => {
const config = upgradeTagConfig[toLevel] || upgradeTagConfig[2]
return `${config.color.bg} ${config.color.text}`
}
// 获取升级 tag 小圆点颜色
const getUpgradeTagDot = (toLevel) => {
const config = upgradeTagConfig[toLevel] || upgradeTagConfig[2]
return config.color.dot
}
// Tab 切换
const onTabChange = (name) => {
// 重置分页和数据
page.value = 1
finished.value = false
data.value = {
total: 0,
list: []
}
// 重新加载数据
getData()
}
// 加载更多数据
@@ -87,24 +170,73 @@ const onLoad = async () => {
// 获取数据
const getData = async () => {
if (loading.value || finished.value) return
try {
loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/rewards?page=${page.value}&page_size=${pageSize.value}`
).get().json()
const params = {
page: page.value,
page_size: pageSize.value
}
let res, error
// 根据 tab 类型调用不同的接口
if (activeTab.value === 'upgrade') {
// 升级返佣:调用升级返佣接口
const result = await getUpgradeRebateList(params)
res = result.data
error = result.error
} else {
// 推广返佣:调用推广返佣接口
const result = await getRebateList(params)
res = result.data
error = result.error
}
if (res.value?.code === 200 && !error.value) {
if (page.value === 1) {
data.value = res.value.data
} else {
data.value.list.push(...res.value.data.list)
let list = res.value.data.list || []
let total = res.value.data.total
// 升级返佣需要转换数据格式
if (activeTab.value === 'upgrade') {
list = list.map(item => ({
id: item.id,
source_agent_id: item.source_agent_id,
source_agent_mobile: item.source_agent_mobile,
source_agent_level: 0, // 升级返佣不需要显示来源代理等级
order_no: item.order_no,
rebate_type: 4, // 升级返佣类型(用于内部标识,不显示)
amount: item.amount,
create_time: item.create_time,
from_level: item.from_level,
to_level: item.to_level
}))
}
if (data.value.list.length >= res.value.data.total ||
res.value.data.list.length < pageSize.value) {
if (page.value === 1) {
data.value = {
total: total,
list: list
}
} else {
data.value.list.push(...list)
}
// 判断是否加载完成
if (data.value.list.length >= total || list.length < pageSize.value) {
finished.value = true
}
} else {
// 接口返回错误或请求失败,停止翻页
finished.value = true
console.error('获取返佣列表失败:', res.value?.msg || error.value || '未知错误')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取返佣列表失败:', err)
} finally {
loading.value = false
}

388
src/views/AgentUpgrade.vue Normal file
View File

@@ -0,0 +1,388 @@
<template>
<div class="p-4 min-h-screen pb-24" style="background: linear-gradient(to bottom, #fef3c7, #fef9e7);">
<!-- 当前等级卡片 -->
<div class="rounded-xl shadow-lg mb-4 bg-white p-6">
<div class="flex items-center gap-4">
<div class="level-badge" :class="getLevelBadgeClass(level)">
{{ getLevelName(level) }}
</div>
<div class="flex-1">
<p class="text-lg font-bold" style="color: var(--van-text-color);">当前等级</p>
<p class="text-sm mt-1" style="color: var(--van-text-color-2);">{{ getLevelDesc(level) }}</p>
</div>
</div>
</div>
<!-- 升级选项 -->
<div class="mb-4">
<h2 class="text-lg font-bold mb-4" style="color: #92400e;">选择升级等级</h2>
<!-- 黄金代理选项 -->
<div v-if="canUpgradeToGold" class="rounded-xl shadow-lg mb-4 bg-white p-6 cursor-pointer border-2"
:style="selectedToLevel === 2 ? 'border-color: #d4af37;' : 'border-color: transparent;'"
@click="selectUpgrade(2)">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-2">
<van-icon name="medal" size="20" color="#f59e0b" />
<span class="text-lg font-bold" style="color: var(--van-text-color);">黄金代理</span>
<van-icon v-if="selectedToLevel === 2" name="checked" size="20" color="#f59e0b" />
</div>
<div class="text-right">
<div class="text-xs mb-1" style="color: var(--van-text-color-2);">升级费用</div>
<div class="text-2xl font-bold" style="color: #f59e0b;">¥{{ goldPrice }}</div>
</div>
</div>
<div v-if="getPrivilegeInfo(2)" class="space-y-2">
<!-- 底价降低 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>底价降低¥{{ getPriceReduction(2) }}</span>
</div>
<!-- 推广返佣 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>{{ getPrivilegeInfo(2).promote_rebate }}</span>
</div>
<!-- 下级升级返佣 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>{{ getPrivilegeInfo(2).upgrade_rebate }}</span>
</div>
<!-- 特权列表 -->
<div v-for="(privilege, index) in getPrivilegeInfo(2).privileges" :key="index"
class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>{{ privilege }}</span>
</div>
</div>
</div>
<!-- 钻石代理选项 -->
<div v-if="canUpgradeToDiamond" class="rounded-xl shadow-lg mb-4 bg-white p-6 cursor-pointer border-2"
:style="selectedToLevel === 3 ? 'border-color: #d4af37;' : 'border-color: transparent;'"
@click="selectUpgrade(3)">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-2">
<van-icon name="medal" size="20" color="#f59e0b" />
<span class="text-lg font-bold" style="color: var(--van-text-color);">钻石代理</span>
<van-icon v-if="selectedToLevel === 3" name="checked" size="20" color="#f59e0b" />
</div>
<div class="text-right">
<div class="text-xs mb-1" style="color: var(--van-text-color-2);">升级费用</div>
<div class="text-2xl font-bold" style="color: #f59e0b;">¥{{ diamondPrice }}</div>
</div>
</div>
<div v-if="getPrivilegeInfo(3)" class="space-y-2">
<!-- 底价降低 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>底价降低¥{{ getPriceReduction(3) }}</span>
</div>
<!-- 推广返佣 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>{{ getPrivilegeInfo(3).promote_rebate }}</span>
</div>
<!-- 下级升级返佣 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>{{ getPrivilegeInfo(3).upgrade_rebate }}</span>
</div>
<!-- 特权列表 -->
<div v-for="(privilege, index) in getPrivilegeInfo(3).privileges" :key="index"
class="flex items-center gap-2 text-sm" style="color: var(--van-text-color-2);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>{{ privilege }}</span>
</div>
</div>
</div>
<!-- 已是最高等级提示 -->
<div v-if="!canUpgradeToGold && !canUpgradeToDiamond" class="rounded-xl shadow-lg bg-white p-8 text-center">
<van-icon name="medal" size="48" color="#f59e0b" />
<p class="text-lg font-bold mt-4" style="color: var(--van-text-color);">您已是最高等级</p>
<p class="text-sm mt-2" style="color: var(--van-text-color-2);">钻石代理享有最高权益</p>
</div>
</div>
<!-- 底部固定按钮区域 -->
<div class="fixed bottom-0 left-0 right-0 bg-white border-t p-4"
style="border-color: #fef3c7; box-shadow: 0 -2px 10px rgba(0,0,0,0.05);">
<div class="flex items-center gap-3">
<!-- 联系客服按钮 -->
<van-button round size="small" class="flex-shrink-0"
style="background-color: #fef3c7; color: #92400e; border: 1px solid #fde68a;" @click="toService">
<van-icon name="service" class="mr-1" />
联系客服
</van-button>
<!-- 确认开通按钮 -->
<van-button type="primary" block round :disabled="!selectedToLevel" :loading="isSubmitting"
style="background: linear-gradient(135deg, #f59e0b, #d97706); border: none;"
@click="confirmUpgrade">
确认开通
</van-button>
</div>
</div>
<!-- 支付弹窗 -->
<Payment v-model="showPayment" :data="paymentData" :id="paymentId" type="agent_upgrade" />
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useAgentStore } from '@/stores/agentStore'
import { applyUpgrade, getAgentInfo, getLevelPrivilege } from '@/api/agent'
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant'
import Payment from '@/components/Payment.vue'
import useApiFetch from '@/composables/useApiFetch'
const router = useRouter()
const agentStore = useAgentStore()
const { level } = storeToRefs(agentStore)
// 升级价格与返佣(由后端配置提供)
const goldPrice = ref(0)
const diamondPrice = ref(0)
const goldRebate = ref(0)
const diamondRebate = ref(0)
const showPayment = ref(false)
const paymentData = ref({})
const paymentId = ref('')
const selectedToLevel = ref(0)
const isSubmitting = ref(false)
const levelPrivileges = ref([])
// 计算可升级选项
const canUpgradeToGold = computed(() => {
return level.value === 1 // 普通代理可以升级为黄金
})
const canUpgradeToDiamond = computed(() => {
return level.value === 1 || level.value === 2 // 普通或黄金可以升级为钻石
})
// 获取等级名称
const getLevelName = (lvl) => {
const names = {
1: '普通代理',
2: '黄金代理',
3: '钻石代理'
}
return names[lvl] || '普通代理'
}
// 获取等级描述
const getLevelDesc = (lvl) => {
const descs = {
1: '基础代理特权',
2: '高级代理特权',
3: '尊享代理特权'
}
return descs[lvl] || '基础代理特权'
}
// 获取等级徽章样式类
const getLevelBadgeClass = (lvl) => {
const classes = {
1: 'normal',
2: 'gold',
3: 'diamond'
}
return classes[lvl] || 'normal'
}
// 选择升级等级(只设置选中状态)
const selectUpgrade = (toLevel) => {
if (selectedToLevel.value === toLevel) {
// 如果已选中,再次点击取消选中
selectedToLevel.value = 0
} else {
selectedToLevel.value = toLevel
}
}
// 计算升级费用和返佣
const getUpgradeInfo = (toLevel) => {
let upgradeFee = 0
let rebateAmount = 0
if (level.value === 1) {
// 普通代理
if (toLevel === 2) {
upgradeFee = goldPrice.value
rebateAmount = goldRebate.value
} else if (toLevel === 3) {
upgradeFee = diamondPrice.value
rebateAmount = diamondRebate.value
}
} else if (level.value === 2) {
// 黄金代理
if (toLevel === 3) {
upgradeFee = diamondPrice.value
rebateAmount = diamondRebate.value
}
}
return { upgradeFee, rebateAmount }
}
// 确认开通
const confirmUpgrade = async () => {
if (!selectedToLevel.value) {
showFailToast('请选择要升级的等级')
return
}
const { upgradeFee, rebateAmount } = getUpgradeInfo(selectedToLevel.value)
// 确认升级
let confirmed = false
try {
await showConfirmDialog({
title: '确认升级',
message: `确定要升级为${getLevelName(selectedToLevel.value)}吗?\n升级费用¥${upgradeFee.toFixed(2)}`,
})
confirmed = true
} catch {
confirmed = false
}
if (!confirmed) return
isSubmitting.value = true
// 申请升级
try {
const { data, error } = await applyUpgrade({
to_level: selectedToLevel.value
})
if (data.value && !error.value) {
if (data.value.code === 200) {
const upgradeId = data.value.data.upgrade_id
const orderNo = data.value.data.order_no
// 如果返回了订单号,说明已经创建了支付订单,直接跳转到支付结果
if (orderNo) {
router.push({
path: '/payment/result',
query: { orderNo }
})
return
}
// 否则打开支付弹窗
paymentData.value = {
product_name: `升级为${getLevelName(selectedToLevel.value)}`,
sell_price: upgradeFee
}
paymentId.value = String(upgradeId)
showPayment.value = true
} else {
showFailToast(data.value.msg || '申请升级失败')
}
} else {
showFailToast('申请升级失败,请重试')
}
} catch (err) {
console.error('申请升级失败:', err)
showFailToast('申请升级失败,请重试')
} finally {
isSubmitting.value = false
}
}
// 获取特权信息
const getPrivilegeInfo = (targetLevel) => {
return levelPrivileges.value.find(p => p.level === targetLevel)
}
// 计算底价降低(使用后端返回的值)
const getPriceReduction = (targetLevel) => {
const privilegeInfo = levelPrivileges.value.find(p => p.level === targetLevel)
if (privilegeInfo && privilegeInfo.price_reduction > 0) {
return privilegeInfo.price_reduction.toFixed(0)
}
return '0'
}
// 加载特权信息
const loadPrivilegeInfo = async () => {
try {
const { data, error } = await getLevelPrivilege()
if (data.value && !error.value && data.value.code === 200) {
levelPrivileges.value = data.value.data.levels || []
goldPrice.value = Number(data.value.data.upgrade_to_gold_fee) || 0
diamondPrice.value = Number(data.value.data.upgrade_to_diamond_fee) || 0
goldRebate.value = Number(data.value.data.upgrade_to_gold_rebate) || 0
diamondRebate.value = Number(data.value.data.upgrade_to_diamond_rebate) || 0
}
} catch (err) {
console.error('获取特权信息失败:', err)
}
}
// 联系客服
const toService = () => {
window.location.href = "https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3"
}
onMounted(() => {
// 加载特权信息
loadPrivilegeInfo()
// 检查是否是代理
if (!agentStore.isAgent) {
showFailToast('您还不是代理')
router.back()
return
}
// 如果已是最高等级,提示
if (level.value === 3) {
// 已经是钻石代理,不需要升级
return
}
// 默认选中第一个可升级选项
if (level.value === 1) {
// 普通代理,默认选中黄金
selectedToLevel.value = 2
} else if (level.value === 2) {
// 黄金代理,默认选中钻石
selectedToLevel.value = 3
}
})
</script>
<style scoped>
.level-badge {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
color: white;
flex-shrink: 0;
}
.level-badge.normal {
background-color: #6b7280;
}
.level-badge.gold {
background-color: #f59e0b;
}
.level-badge.diamond {
background-color: #f59e0b;
}
</style>

View File

@@ -1,39 +0,0 @@
<template>
<div class="relative">
<img class="" src="@/assets/images/vip_bg.png" alt="代理会员" />
<div
class="absolute left-[50%] translate-x-[-50%] bottom-80 flex flex-col gap-4 items-center"
>
<div
@click="toVipApply"
class="bg-gradient-to-r from-amber-500 to-amber-600 py-2 px-6 rounded-lg text-white text-[24px] font-bold shadow-[0_0_15px_rgba(255,255,255,0.3)] hover:scale-105 transition-transform"
>
申请VIP代理
</div>
<div
@click="toService"
class="bg-gradient-to-r from-gray-900 via-black to-gray-900 py-2 px-4 rounded-lg text-white text-[20px] font-bold shadow-[0_0_15px_rgba(255,255,255,0.3)] hover:scale-105 transition-transform"
>
联系客服
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
function toVipApply() {
router.push("/agent/vipApply");
}
function toService() {
// window.location.href = '/service' // 跳转到客服页面
window.location.href =
"https://work.weixin.qq.com/kfid/kfc8a32720024833f57"; // 跳转到客服页面
}
</script>
<style lang="scss" scoped></style>

View File

@@ -1,907 +0,0 @@
<template>
<div class="agent-VIP-apply w-full min-h-screen bg-gradient-to-b from-amber-50 via-amber-100 to-amber-50 pb-24">
<!-- 装饰元素 -->
<div
class="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-amber-300 to-amber-500 rounded-bl-full opacity-20">
</div>
<div
class="absolute top-40 left-0 w-16 h-16 bg-gradient-to-tr from-amber-400 to-amber-600 rounded-tr-full opacity-20">
</div>
<div
class="absolute bottom-60 right-0 w-24 h-24 bg-gradient-to-bl from-amber-300 to-amber-500 rounded-tl-full opacity-20">
</div>
<!-- 顶部标题区域 -->
<div class="header relative pt-8 px-4 pb-6 text-center">
<div
class="animate-pulse absolute -top-2 left-1/2 -translate-x-1/2 w-24 h-1 bg-gradient-to-r from-amber-300 via-amber-500 to-amber-300 rounded-full">
</div>
<h1 class="text-3xl font-bold text-amber-800 mb-1">
{{ isVipOrSvip ? '代理会员续费' : 'VIP代理申请' }}
</h1>
<p class="text-sm text-amber-700 mt-2 max-w-xs mx-auto">
<template v-if="isVipOrSvip">
您的会员有效期至 {{ formatExpiryTime(ExpiryTime) }}续费后有效期至
{{ renewalExpiryTime }}
</template>
<template v-else>
平台为疯狂推广者定制的赚买计划助您收益<span class="text-red-500 font-bold">翻倍增升</span>
</template>
</p>
<!-- 装饰性金币图标 -->
<div class="absolute top-6 left-4 transform -rotate-12">
<div
class="w-8 h-8 bg-gradient-to-br from-yellow-300 to-yellow-500 rounded-full flex items-center justify-center shadow-lg">
<span class="text-white font-bold text-xs">¥</span>
</div>
</div>
<div class="absolute top-10 right-6 transform rotate-12">
<div
class="w-6 h-6 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full flex items-center justify-center shadow-lg">
<span class="text-white font-bold text-xs">¥</span>
</div>
</div>
</div>
<!-- 选择代理类型 -->
<div class="card-container px-4 mb-8">
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100 transform transition-all">
<h2
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<span class="relative z-10">选择代理类型</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</div>
</div>
</h2>
<div class="flex p-6 gap-4">
<div
class="flex-1 border-2 rounded-lg p-4 text-center cursor-pointer transition-all duration-300 relative transform hover:-translate-y-1"
:class="[
selectedType === 'vip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('vip')">
<div class="text-xl font-bold text-amber-700">VIP代理</div>
<div class="text-amber-600 font-bold mt-1 text-lg">{{ vipConfig.price }}{{ vipConfig.priceUnit }}</div>
<div class="mt-2 text-gray-600 text-sm">标准VIP权益</div>
<div v-if="selectedType === 'vip'"
class="absolute -top-2 -right-2 w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center shadow-md">
<van-icon name="success" color="#fff" size="14" />
</div>
</div>
<div
class="flex-1 border-2 rounded-lg p-4 text-center cursor-pointer transition-all duration-300 relative transform hover:-translate-y-1"
:class="[
selectedType === 'svip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('svip')">
<div class="text-xl font-bold text-amber-700">SVIP代理</div>
<div class="text-amber-600 font-bold mt-1 text-lg">{{ vipConfig.svipPrice }}{{ vipConfig.priceUnit }}</div>
<div class="mt-2 text-gray-600 text-sm">超级VIP权益</div>
<div v-if="selectedType === 'svip'"
class="absolute -top-2 -right-2 w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center shadow-md">
<van-icon name="success" color="#fff" size="14" />
</div>
</div>
</div>
</div>
</div>
<!-- 六大超值权益 -->
<div class="card-container px-4 mb-8">
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100">
<h2
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<span class="relative z-10">六大超值权益</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</div>
</div>
</h2>
<div class="grid grid-cols-2 gap-4 p-4">
<!-- 权益1 -->
<div
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<div class="text-amber-800 font-bold mb-2 flex items-center">
<span
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">1</span>
下级贡献收益
</div>
<p class="text-sm text-gray-600">
下级完全收益您来定涨多少赚多少一单最高收益<span class="text-red-500 font-bold">10</span>
</p>
</div>
<!-- 权益2 -->
<div
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<div class="text-amber-800 font-bold mb-2 flex items-center">
<span
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">2</span>
下级提现收益
</div>
<p class="text-sm text-gray-600">
下级定价标准由您定超过标准部分收益更丰厚一单最高多赚<span class="text-red-500 font-bold">10</span>
</p>
</div>
<!-- 权益3 -->
<div
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<div class="text-amber-800 font-bold mb-2 flex items-center">
<span
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">3</span>
转换高额奖励
</div>
<p class="text-sm text-gray-600">
下级成为VIPSVIP高额奖励立马发放<span class="text-red-500 font-bold">399</span>
</p>
</div>
<!-- 权益4 -->
<div
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<div class="text-amber-800 font-bold mb-2 flex items-center">
<span
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">4</span>
下级提现奖励
</div>
<p class="text-sm text-gray-600">下级成为SVIP每次提现都奖励1%坐享被动收入</p>
</div>
<!-- 权益5 -->
<div
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<div class="text-amber-800 font-bold mb-2 flex items-center">
<span
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">5</span>
月度现金奖励
</div>
<p class="text-sm text-gray-600">
下级每月活跃达100名额外奖励<span class="text-red-500 font-bold">50</span>新增15名活跃下级再得<span
class="text-red-500 font-bold">50</span>
</p>
</div>
<!-- 权益6 -->
<div
class="bg-gradient-to-br from-amber-50 to-amber-100 rounded-lg p-3 border border-amber-200 transition-all duration-300 hover:shadow-md hover:border-amber-300">
<div class="text-amber-800 font-bold mb-2 flex items-center">
<span
class="w-6 h-6 bg-gradient-to-br from-amber-500 to-amber-600 rounded-full flex items-center justify-center text-white text-xs mr-2">6</span>
平台专项扶持
</div>
<p class="text-sm text-gray-600">一对一专属客服服务为合作伙伴提供全方位成长赋能</p>
</div>
</div>
</div>
</div>
<!-- 权益对比表 -->
<div class="card-container px-4 mb-8" v-if="selectedType">
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100">
<h2
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<span class="relative z-10">{{ selectedType === 'vip' ? 'VIP' : 'SVIP' }}代理权益对比</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</div>
</div>
</h2>
<div class="p-4 overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gradient-to-r from-amber-100 to-amber-200">
<th class="border border-amber-200 p-2 text-left text-amber-800">权益项目</th>
<th class="border border-amber-200 p-2 text-center text-amber-800">普通代理</th>
<th class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }">
VIP代理
</th>
<th class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }">
SVIP代理
</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">会员权益</td>
<td class="border border-amber-200 p-2 text-center">普通代理<br />免费</td>
<td class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.price }}{{ vipConfig.priceUnit }}
</td>
<td class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipPrice }}{{ vipConfig.priceUnit }}
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级贡献收益</td>
<td class="border border-amber-200 p-2 text-center">1/</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipCommission }}/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipCommission }}/
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">自定义设置下级成本</td>
<td class="border border-amber-200 p-2 text-center"></td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级价格浮动收益</td>
<td class="border border-amber-200 p-2 text-center"></td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
最高{{ vipConfig.vipFloatingRate }}%
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
最高{{ vipConfig.svipFloatingRate }}%
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级提现奖励</td>
<td class="border border-amber-200 p-2 text-center"></td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.withdrawRatio }}%
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级活跃奖励</td>
<td class="border border-amber-200 p-2 text-center"></td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.monthlyRewardForTeam }}/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.monthlyRewardForTeam }}/
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">新增活跃奖励</td>
<td class="border border-amber-200 p-2 text-center"></td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.monthlyRewardForNewTeam }}/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.monthlyRewardForNewTeam }}/
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级转化奖励</td>
<td class="border border-amber-200 p-2 text-center"></td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipConversionBonus }}*10
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipConversionBonus }}*10
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">提现次数额度</td>
<td class="border border-amber-200 p-2 text-center">800/</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipWithdrawalLimit }}/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipWithdrawalLimit }}/
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">提现次数</td>
<td class="border border-amber-200 p-2 text-center">1/</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
1/
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
2/
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 收益预估 -->
<div class="card-container px-4 mb-8" v-if="selectedType">
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-amber-100">
<h2
class="bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 text-center font-bold relative overflow-hidden">
<span class="relative z-10">收益预估对比</span>
<div class="absolute inset-0 bg-amber-500 opacity-30">
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</div>
</div>
</h2>
<div class="p-4">
<!-- 顶部收益概览 -->
<div class="mb-6 rounded-lg overflow-hidden border border-amber-200">
<div class="bg-gradient-to-r from-amber-100 to-amber-200 py-2 px-4 text-center font-bold text-amber-800">
VIP与SVIP代理收益对比
</div>
<div class="grid grid-cols-2 divide-x divide-amber-200">
<div class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'vip' }">
<div class="text-sm text-gray-600 mb-1">VIP月预计收益</div>
<div class="text-amber-600 font-bold text-xl">{{ revenueData.vipMonthly }}</div>
<div class="text-xs text-gray-500 mt-1">年收益{{ revenueData.vipYearly }}</div>
</div>
<div class="p-4 text-center" :class="{ 'bg-amber-50': selectedType === 'svip' }">
<div class="text-sm text-gray-600 mb-1">SVIP月预计收益</div>
<div class="text-red-500 font-bold text-xl">{{ revenueData.svipMonthly }}</div>
<div class="text-xs text-gray-500 mt-1">年收益{{ revenueData.svipYearly }}</div>
</div>
</div>
<div class="bg-gradient-to-r from-red-50 to-red-100 py-2 px-4 text-center text-red-600 font-medium">
选择SVIP相比VIP月增收益<span class="font-bold">{{ revenueData.monthlyDifference }}</span>
</div>
</div>
<!-- 详细收益表格 -->
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gradient-to-r from-amber-100 to-amber-200">
<th class="border border-amber-200 p-2 text-left text-amber-800">收益来源</th>
<th class="border border-amber-200 p-2 text-center text-amber-800 w-1/3"
:class="{ 'bg-amber-200': selectedType === 'vip' }">
VIP代理
</th>
<th class="border border-amber-200 p-2 text-center text-amber-800 w-1/3"
:class="{ 'bg-amber-200': selectedType === 'svip' }">
SVIP代理
</th>
</tr>
</thead>
<tbody>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">推广收益()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
300×50=15,000
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
300×50=15,000
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级贡献收益()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
300×{{ vipConfig.vipCommission }}=360
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
300×{{ vipConfig.svipCommission }}=450
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级价格浮动收益()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
100×100×{{ vipConfig.vipFloatingRate }}%=500
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
200×100×{{ vipConfig.svipFloatingRate }}%=2,000
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级提现奖励()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
-
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ revenueData.withdrawReward }}
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级活跃奖励()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.monthlyRewardForTeam }}
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.monthlyRewardForTeam }}
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">新增活跃奖励()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.monthlyRewardForNewTeam }}
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.monthlyRewardForNewTeam }}
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">下级转化奖励()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipConversionBonus }}×2=598
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipConversionBonus }}×2=798
</td>
</tr>
<tr class="hover:bg-amber-50 transition-colors">
<td class="border border-amber-200 p-2 font-medium">额外业务收益()</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
约3,000
</td>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
约6,000
</td>
</tr>
<tr class="bg-gradient-to-r from-amber-50 to-amber-100 font-bold">
<td class="border border-amber-200 p-3">月计收益</td>
<td class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}">
{{ revenueData.vipMonthly }}
</td>
<td class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}">
{{ revenueData.svipMonthly }}
</td>
</tr>
<tr class="bg-gradient-to-r from-amber-50 to-amber-100 font-bold">
<td class="border border-amber-200 p-3">年计收益</td>
<td class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}">
{{ revenueData.vipYearly }}
</td>
<td class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}">
{{ revenueData.svipYearly }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- 投资回报率 -->
<div class="mt-6 p-4 bg-gradient-to-r from-amber-50 to-amber-100 rounded-lg border border-amber-200">
<div class="text-center mb-3 font-bold text-amber-800">投资收益分析</div>
<div class="grid grid-cols-1 gap-4">
<div class="p-3 bg-white rounded-lg shadow-sm">
<div class="flex items-center justify-between">
<div class="flex-1 border-r border-amber-100 pr-3">
<div class="text-amber-700 font-medium text-center mb-1">VIP方案</div>
<div class="text-center">
<div class="text-amber-600 text-sm">投资{{ vipConfig.price }}</div>
<div class="text-gray-600 text-sm">月收益{{ revenueData.vipMonthly }}</div>
</div>
</div>
<div class="flex-1 pl-3">
<div class="text-red-500 font-medium text-center mb-1">SVIP方案</div>
<div class="text-center">
<div class="text-red-500 text-sm">投资{{ vipConfig.svipPrice }}</div>
<div class="text-gray-600 text-sm">月收益{{ revenueData.svipMonthly }}</div>
</div>
</div>
</div>
</div>
<!-- 升级收益对比 -->
<div class="p-3 bg-gradient-to-r from-red-50 to-amber-50 rounded-lg shadow-sm">
<div class="text-center font-medium text-red-700 mb-2">SVIP升级优势分析</div>
<div class="flex items-center justify-center gap-3">
<div class="text-center">
<div class="text-sm text-gray-600">额外投资</div>
<div class="text-red-600 font-bold">{{ revenueData.priceDifference }}</div>
</div>
<div
class="bg-red-500 flex-shrink-0 text-white rounded-full w-6 h-6 flex items-center justify-center">
<div class="transform -translate-y-px"></div>
</div>
<div class="text-center">
<div class="text-sm text-gray-600">每月额外收益</div>
<div class="text-red-600 font-bold">{{ revenueData.monthlyDifference }}</div>
</div>
<div
class="bg-red-500 flex-shrink-0 text-white rounded-full w-6 h-6 flex items-center justify-center">
<span class="transform -translate-y-px"></span>
</div>
<div class="text-center">
<div class="text-sm text-gray-600">投资回收时间</div>
<div class="text-red-600 font-bold">{{ revenueData.recoverDays }}</div>
</div>
</div>
<div class="text-center text-red-500 font-medium mt-3">
额外投资{{ revenueData.priceDifference }}<span class="text-red-600 font-bold">年多赚{{
revenueData.yearlyDifference }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 申请按钮固定在底部 -->
<div
class="fixed bottom-0 left-0 right-0 px-4 py-3 bg-gradient-to-t from-amber-100 to-transparent backdrop-blur-sm z-30">
<div class="flex flex-col gap-2">
<button :class="buttonClass" @click="applyVip" :disabled="!canPerformAction">
<span class="relative z-10">{{ buttonText }}</span>
<div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white to-transparent opacity-20 transform -skew-x-30 translate-x-full animate-shimmer">
</div>
</button>
<button
class="w-full py-3 rounded-lg font-medium text-amber-700 border border-amber-400 bg-white shadow-md active:bg-amber-50 transform transition-transform active:scale-98"
@click="contactService">
<div class="flex items-center justify-center">
<van-icon name="service-o" class="mr-1" />
<span>联系客服咨询</span>
</div>
</button>
<!-- 最终解释权声明 -->
<div class="text-center text-xs text-gray-400 py-1">最终解释权归海南省学宇思网络科技有限公司所有</div>
</div>
</div>
</div>
<Payment v-model="showPayment" :data="payData" :id="payID" type="agent_vip" @close="showPayment = false" />
</template>
<script setup>
import { ref, onMounted, reactive, computed } from 'vue'
import { showToast } from 'vant'
import { useAgentStore } from '@/stores/agentStore'
import { storeToRefs } from 'pinia'
// 获取代理状态
const agentStore = useAgentStore()
const { level, ExpiryTime } = storeToRefs(agentStore)
// 计算是否已经是VIP或SVIP
const isVipOrSvip = computed(() => ['VIP', 'SVIP'].includes(level.value))
const isVip = computed(() => level.value === 'VIP')
const isSvip = computed(() => level.value === 'SVIP')
// 计算续费后的到期时间
const renewalExpiryTime = computed(() => {
if (!ExpiryTime.value) return '未知'
// 从格式化字符串中提取日期部分
const dateStr = ExpiryTime.value.split(' ')[0] // 假设格式是 "YYYY-MM-DD HH:MM:SS"
const [year, month, day] = dateStr.split('-').map(num => parseInt(num))
// 创建日期对象并加一年
const expiryDate = new Date(year, month - 1, day) // 月份从0开始所以要-1
expiryDate.setFullYear(expiryDate.getFullYear() + 1)
// 返回格式化的日期字符串
return `${expiryDate.getFullYear()}-${String(expiryDate.getMonth() + 1).padStart(2, '0')}-${String(expiryDate.getDate()).padStart(2, '0')}`
})
// 按钮文字 - 根据当前状态显示不同文案
const buttonText = computed(() => {
if (!isVipOrSvip.value) return '立即开通' // 非会员状态
if (selectedType.value === 'vip') {
if (isVip.value) return '续费VIP代理' // VIP续费VIP
return '降级不可用' // SVIP不能降级到VIP
} else {
if (isSvip.value) return '续费SVIP代理' // SVIP续费SVIP
return '升级SVIP代理' // VIP升级SVIP
}
})
// 是否可以操作按钮
const canPerformAction = computed(() => {
// 非会员可以开通任何会员
if (!isVipOrSvip.value) return true
// VIP不能降级到普通会员
if (isVip.value && selectedType.value === '') return false
// SVIP不能降级到VIP
if (isSvip.value && selectedType.value === 'vip') return false
return true
})
// 计算按钮类名
const buttonClass = computed(() => {
const baseClass =
'w-full py-4 rounded-lg font-bold text-lg shadow-lg transform transition-transform active:scale-98 relative overflow-hidden'
if (!canPerformAction.value) {
return `${baseClass} bg-gray-400 text-white cursor-not-allowed`
}
if (isVip.value && selectedType.value === 'svip') {
return `${baseClass} bg-gradient-to-r from-purple-500 to-indigo-600 text-white`
}
return `${baseClass} bg-gradient-to-r from-amber-500 to-amber-600 active:from-amber-600 active:to-amber-700 text-white`
})
// VIP价格配置
const vipConfig = reactive({})
// 计算得出的收益数据
const revenueData = computed(() => {
const baseOrders = 300 // 基础订单数
const pricePerOrder = 50 // 每单价格
const baseRevenue = baseOrders * pricePerOrder // 基础推广收益
const vipCommissionRevenue = baseOrders * vipConfig.vipCommission // VIP下级贡献收益
const svipCommissionRevenue = baseOrders * vipConfig.svipCommission // SVIP下级贡献收益
const vipFloatingRevenue = 100 * 100 * (vipConfig.vipFloatingRate / 100) // VIP浮动收益
const svipFloatingRevenue = 200 * 100 * (vipConfig.svipFloatingRate / 100) // SVIP浮动收益
const vipConversionRevenue = vipConfig.vipConversionBonus * 2 // VIP转化奖励
const svipConversionRevenue = vipConfig.svipConversionBonus * 2 // SVIP转化奖励
const vipExtraRevenue = 3000 // VIP额外收益估计
const svipExtraRevenue = 6000 // SVIP额外收益估计
// 平级提现奖励(只有SVIP才有)
const withdrawReward = 20000 * (vipConfig.withdrawRatio / 100)
// 计算月总收益
const vipMonthlyTotal =
baseRevenue +
vipCommissionRevenue +
vipFloatingRevenue +
vipConfig.monthlyRewardForTeam +
vipConfig.monthlyRewardForNewTeam +
vipConversionRevenue +
vipExtraRevenue
const svipMonthlyTotal =
baseRevenue +
svipCommissionRevenue +
svipFloatingRevenue +
withdrawReward +
vipConfig.monthlyRewardForTeam +
vipConfig.monthlyRewardForNewTeam +
svipConversionRevenue +
svipExtraRevenue
// 计算VIP和SVIP之间的差额
const monthlyDifference = svipMonthlyTotal - vipMonthlyTotal
const priceDifference = vipConfig.svipPrice - vipConfig.price
return {
vipMonthly: Math.round(vipMonthlyTotal),
svipMonthly: Math.round(svipMonthlyTotal),
vipYearly: Math.round(vipMonthlyTotal * 12),
svipYearly: Math.round(svipMonthlyTotal * 12),
monthlyDifference: Math.round(monthlyDifference),
yearlyDifference: Math.round(monthlyDifference * 12),
vipRate: Math.round(vipMonthlyTotal / vipConfig.price),
svipRate: Math.round(svipMonthlyTotal / vipConfig.svipPrice),
priceDifference,
recoverDays: Math.ceil(priceDifference / (monthlyDifference / 30)),
withdrawReward,
}
})
// 加载价格配置
onMounted(async () => {
document.documentElement.style.scrollBehavior = 'smooth'
// 从API获取会员配置信息
try {
const { data, error } = await useApiFetch('/agent/membership/info').get().json()
if (data.value && !error.value && data.value.code === 200) {
const configData = data.value.data
// 更新VIP配置
if (configData.vip_config) {
const vipData = configData.vip_config
vipConfig.price = vipData.price
vipConfig.vipCommission = vipData.report_commission // 直接显示,就是几元/单
vipConfig.vipFloatingRate = vipData.price_ratio * 100 // 转换为百分比
vipConfig.monthlyRewardForTeam = vipData.lower_activity_reward
vipConfig.monthlyRewardForNewTeam = vipData.new_activity_reward
vipConfig.vipConversionBonus = vipData.lower_convert_vip_reward
vipConfig.vipWithdrawalLimit = vipData.exemption_amount
}
// 更新SVIP配置
if (configData.svip_config) {
const svipData = configData.svip_config
vipConfig.svipPrice = svipData.price
vipConfig.svipCommission = svipData.report_commission // 直接显示,就是几元/单
vipConfig.svipFloatingRate = svipData.price_ratio * 100 // 转换为百分比
vipConfig.withdrawRatio = svipData.lower_withdraw_reward_ratio * 100 // 转换为百分比
vipConfig.monthlyRewardForTeam = svipData.lower_activity_reward
vipConfig.monthlyRewardForNewTeam = svipData.new_activity_reward
vipConfig.svipConversionBonus = svipData.lower_convert_svip_reward
vipConfig.svipWithdrawalLimit = svipData.exemption_amount
}
console.log('会员配置信息加载成功', configData)
} else {
console.error('获取会员配置信息失败', data.value?.msg || '未知错误')
}
} catch (error) {
console.error('加载会员配置信息失败', error)
}
})
const selectedType = ref('vip') // 默认选择VIP
const showPayment = ref(false)
const payData = ref({
product_name: `${selectedType.value.toUpperCase()}代理`,
sell_price: vipConfig.price,
})
const payID = ref('')
// 选择代理类型
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
async function applyVip() {
// 如果是VIP想升级到SVIP提示联系客服
if (isVip.value && selectedType.value === 'svip') {
contactService()
return
}
// 如果是SVIP要降级到VIP提示不能降级
if (isSvip.value && selectedType.value === 'vip') {
showToast('SVIP会员不能降级到VIP会员')
return
}
const { data, error } = await useApiFetch('/agent/membership/activate')
.post({
type: selectedType.value.toUpperCase(),
})
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
if (data.value.data.id) {
payID.value = data.value.data.id
showPayment.value = true
}
}
}
}
// 联系客服
function contactService() {
// 跳转到客服页面
window.location.href = 'https://work.weixin.qq.com/kfid/kfc8a32720024833f57'
}
function formatExpiryTime(expiryTimeStr) {
if (!expiryTimeStr) return '未知'
// 从格式化字符串中提取日期部分
return expiryTimeStr.split(' ')[0] // 假设格式是 "YYYY-MM-DD HH:MM:SS"
}
</script>
<style scoped>
.agent-VIP-apply {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
}
@keyframes shimmer {
0% {
transform: translateX(-100%) skewX(-30deg);
}
100% {
transform: translateX(200%) skewX(-30deg);
}
}
.animate-shimmer {
animation: shimmer 3s infinite;
}
.active\:scale-98:active {
transform: scale(0.98);
}
</style>

View File

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

View File

@@ -10,7 +10,7 @@
onMounted(() => {
// 插入客服脚本
(function (d, t) {
var BASE_URL = "https://service.quannengcha.com";
var BASE_URL = "https://service.onecha.cn";
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src = BASE_URL + "/packs/js/sdk.js";

View File

@@ -49,9 +49,7 @@ const categories = [
{
title: '其他',
name: 'other',
items: [
{ id: 'vip_guide', title: '如何成为VIP代理和SVIP代理?' }
]
items: []
}
]

View File

@@ -29,8 +29,7 @@ const imageMap = {
report_push: '/image/help/report-push.jpg',
report_secret: ['/image/help/report-secret-1.jpg', '/image/help/report-secret-2.jpg'],
invite_earnings: '/image/help/invite-earnings.jpg',
direct_earnings: '/image/help/direct-earnings.jpg',
vip_guide: '/image/help/vip-guide.jpg'
direct_earnings: '/image/help/direct-earnings.jpg'
}
// 标题映射
@@ -42,8 +41,7 @@ const titleMap = {
report_push: '如何推广报告',
report_secret: '报告推广秘籍大公开',
invite_earnings: '如何邀请下级成为代理',
direct_earnings: '如何成为一查查代理',
vip_guide: '如何成为VIP代理和SVIP代理?'
direct_earnings: '如何成为一查查代理'
}
onMounted(() => {

View File

@@ -1,6 +1,16 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/userStore'
import { useAppStore } from '@/stores/appStore'
import { useDialogStore } from '@/stores/dialogStore'
import BindPhoneOnlyDialog from '@/components/BindPhoneOnlyDialog.vue'
const router = useRouter()
const userStore = useUserStore()
const appStore = useAppStore()
const dialogStore = useDialogStore()
const { isLoggedIn, mobile } = storeToRefs(userStore)
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
@@ -9,30 +19,48 @@ const num = ref(0)
const max = ref(60)
const loading = ref(false)
const finished = ref(false)
const showBindNotice = computed(() => isLoggedIn.value && !mobile.value && reportList.value.length > 0)
const hasNoRecords = computed(() => reportList.value.length === 0)
// 初始加载数据
async function fetchData() {
loading.value = true
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`)
.get()
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
total.value = data.value.data.total
if (data.value.data.list && data.value.data.list.length > 0) {
reportList.value.push(...data.value.data.list)
page.value += 1
}
if (reportList.value.length >= total.value) {
finished.value = true
}
}
}
loading.value = false
if (loading.value || finished.value) return
loading.value = true
try {
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`)
.get()
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
total.value = data.value.data.total
if (data.value.data.list && data.value.data.list.length > 0) {
reportList.value.push(...data.value.data.list)
page.value += 1
}
if (reportList.value.length >= total.value) {
finished.value = true
}
} else {
// 接口返回错误,停止翻页
finished.value = true
console.error('获取查询列表失败:', data.value.msg || '未知错误')
}
} else {
// 请求失败或返回错误,停止翻页
finished.value = true
console.error('获取查询列表失败:', error.value || '请求失败')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取查询列表失败:', err)
} finally {
loading.value = false
}
}
// 初始加载
onMounted(() => {
onMounted(async () => {
fetchData()
})
@@ -53,6 +81,19 @@ function toDetail(item) {
router.push({ path: '/report', query: { orderId: item.order_id } });
}
function toLogin() {
const redirect = encodeURIComponent('/historyQuery')
router.push({ path: '/login', query: { redirect, from: 'promotionInquire' } })
}
function handleBindSuccess() {
reportList.value = []
page.value = 1
finished.value = false
loading.value = false
fetchData()
}
// 状态文字映射
function stateText(state) {
switch (state) {
@@ -88,6 +129,20 @@ function statusClass(state) {
<template>
<div class="flex flex-col gap-4 p-4">
<BindPhoneOnlyDialog @bind-success="handleBindSuccess" />
<div v-if="showBindNotice"
class="bg-yellow-50 border border-yellow-200 text-yellow-700 rounded-lg p-3 flex items-center justify-between">
<div class="text-sm">为防止报告丢失请绑定手机号</div>
<button class="px-3 py-1 text-sm font-medium bg-blue-500 text-white rounded"
@click="dialogStore.openBindPhone">绑定手机号</button>
</div>
<div class="text-xs text-gray-500">为保障用户隐私及数据安全报告保留{{ appStore.queryRetentionDays || 30 }}过期自动清理</div>
<div v-if="hasNoRecords"
class="bg-white rounded-lg shadow-sm p-6 flex flex-col items-center justify-center gap-3">
<div class="text-gray-600 text-sm">暂无历史报告</div>
<button v-if="!isLoggedIn || !mobile" class="px-4 py-2 bg-blue-500 text-white rounded"
@click="toLogin">登录</button>
</div>
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="item in reportList" :key="item.id" @click="toDetail(item)"
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">

View File

@@ -1,16 +1,41 @@
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import InquireForm from "@/components/InquireForm.vue";
import { ref, onMounted, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useAgentStore } from "@/stores/agentStore";
import { storeToRefs } from "pinia";
import { showToast } from "vant";
const route = useRoute();
const router = useRouter();
const agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore);
const feature = ref(route.params.feature);
// 获取产品信息
const featureData = ref({});
// 检查是否是代理,如果不是则重定向
onMounted(async () => {
// 检查支付回调
isFinishPayment();
// 检查是否是代理,如果不是则提示并重定向
const token = localStorage.getItem("token");
if (token) {
await agentStore.fetchAgentStatus();
if (!isAgent.value) {
showToast({ message: "请先成为代理才能使用查询功能" });
router.replace("/login");
return;
}
} else {
showToast({ message: "请先注册成为代理才能使用查询功能" });
router.replace("/login");
return;
}
// 只有代理才能继续访问
await getProduct();
});
@@ -45,5 +70,18 @@ async function getProduct() {
</script>
<template>
<InquireForm :type="'normal'" :feature="feature" :feature-data="featureData" />
<!-- 普通查询已禁用提示使用推广链接 -->
<div v-if="!isAgent" class="min-h-screen flex items-center justify-center p-6">
<div class="text-center">
<div class="text-lg font-bold mb-4">查询功能仅限代理使用</div>
<div class="text-gray-600 mb-6">请先成为代理或通过推广链接访问查询功能</div>
<button
@click="router.push('/login')"
class="px-6 py-3 bg-primary text-white rounded-lg">
注册成为代理
</button>
</div>
</div>
<!-- 代理可以使用但应该使用推广链接 -->
<InquireForm v-else :type="'normal'" :feature="feature" :feature-data="featureData" />
</template>

View File

@@ -1,44 +1,398 @@
<template>
<div>
<img src="@/assets/images/invitation.png" alt="邀请下级" />
<div
@click="showQRcode = true"
class="bg-gradient-to-t from-orange-500 to-orange-300 fixed bottom-0 h-12 w-full bg-orange-400 shadow-xl text-white rounded-t-xl flex items-center justify-center font-bold"
>
立即邀请好友
<div class="min-h-screen bg-gradient-to-b from-blue-50 to-white pb-20">
<!-- 页面标题 -->
<div class="px-4 pt-6 pb-4">
<h1 class="text-2xl font-bold text-gray-800">邀请下级代理</h1>
<p class="text-sm text-gray-500 mt-2">选择邀请码生成二维码或复制链接</p>
</div>
<div class="px-4 space-y-4">
<!-- 我的邀请码列表 -->
<div class="bg-white rounded-xl shadow-lg p-5">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800">我的邀请码</h3>
<button @click="showGenerateCodeDialog = true"
class="px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-lg text-sm font-medium shadow-md active:from-blue-600 active:to-blue-700">
生成邀请码
</button>
</div>
<div v-if="loadingCodeList" class="text-center py-8 text-gray-500">
加载中...
</div>
<div v-else-if="codeList.length === 0" class="text-center py-8 text-gray-500">
<p>暂无邀请码</p>
<button @click="showGenerateCodeDialog = true"
class="mt-4 px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-lg text-sm font-medium shadow-md active:from-blue-600 active:to-blue-700">
立即生成
</button>
</div>
<div v-else class="space-y-3 max-h-[600px] overflow-y-auto">
<div v-for="item in codeList" :key="item.id" :class="[
'rounded-xl p-4 border-2 transition-all cursor-pointer shadow-sm',
selectedCodeId === item.id
? 'border-blue-500 bg-blue-50 shadow-md'
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow'
]" @click="selectCode(item)">
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<div class="text-lg font-mono font-bold text-blue-600 break-all">{{ item.code }}
</div>
<span v-if="isExpired(item)"
class="text-xs px-2.5 py-1 rounded-lg bg-red-100 text-red-700 font-medium whitespace-nowrap">
已过期
</span>
</div>
<div class="text-xs text-gray-500 space-y-0.5">
<div v-if="item.remark">备注{{ item.remark }}</div>
<div>创建时间{{ item.create_time }}</div>
<div v-if="item.expire_time">过期时间{{ item.expire_time }}</div>
<div v-if="item.used_time">使用时间{{ item.used_time }}</div>
</div>
</div>
<div class="flex items-center gap-2">
<div v-if="selectedCodeId === item.id" class="flex-shrink-0">
<div
class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center shadow-sm">
<span class="text-white text-xs font-bold"></span>
</div>
</div>
<button v-if="selectedCodeId === item.id && item.status === 0"
@click.stop="handleDeleteCode(item.id, item.code)"
class="px-3 py-1.5 bg-red-500 text-white rounded-lg text-xs font-medium active:bg-red-600 whitespace-nowrap shadow-sm">
删除
</button>
</div>
</div>
<div v-if="selectedCodeId === item.id && item.status === 0" class="mt-3 flex gap-2">
<button @click.stop="copyInviteCode(item.code)"
class="flex-1 px-4 py-2.5 bg-blue-500 text-white rounded-lg text-sm font-medium active:bg-blue-600 shadow-sm">
复制邀请码
</button>
<button @click.stop="handleGenerateLink(item.code)"
class="flex-1 px-4 py-2.5 bg-green-500 text-white rounded-lg text-sm font-medium active:bg-green-600 shadow-sm">
生成链接
</button>
<button @click.stop="handleGenerateQRCode(item.code)"
class="flex-1 px-4 py-2.5 bg-orange-500 text-white rounded-lg text-sm font-medium active:bg-orange-600 shadow-sm">
生成二维码
</button>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="codeListTotal > pageSize" class="mt-4 flex items-center justify-between pt-4 border-t">
<button @click="loadCodeList(currentPage - 1)" :disabled="currentPage <= 1"
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium active:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed">
上一页
</button>
<span class="text-sm text-gray-600 font-medium">
{{ currentPage }} / {{ Math.ceil(codeListTotal / pageSize) }}
</span>
<button @click="loadCodeList(currentPage + 1)"
:disabled="currentPage >= Math.ceil(codeListTotal / pageSize)"
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium active:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed">
下一页
</button>
</div>
</div>
<!-- 生成的邀请链接显示区域 -->
<!-- <div v-if="showInviteLink && inviteLink" class="bg-white rounded-xl shadow-lg p-5">
<h3 class="text-lg font-semibold text-gray-800 mb-4">邀请链接</h3>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200 mb-4">
<div class="text-sm text-blue-600 break-all font-mono">{{ inviteLink }}</div>
</div>
<button @click="copyInviteLink"
class="w-full py-3 bg-gradient-to-r from-orange-500 to-orange-600 text-white rounded-lg text-sm font-medium shadow-md active:from-orange-600 active:to-orange-700">
复制链接
</button>
</div> -->
</div>
<!-- 生成邀请码弹窗 -->
<van-dialog v-model:show="showGenerateCodeDialog" title="生成邀请码" show-cancel-button @confirm="handleGenerateCode"
@cancel="showGenerateCodeDialog = false">
<div class="p-4 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">生成数量</label>
<van-stepper v-model="generateCount" :min="1" :max="100" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">过期天数0表示不过期</label>
<van-stepper v-model="expireDays" :min="0" :max="365" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">备注可选</label>
<van-field v-model="remark" placeholder="请输入备注" maxlength="50" />
</div>
</div>
</van-dialog>
<!-- 二维码弹窗 -->
<QRcode v-model:show="showQRcode" mode="invitation" :invite-link="inviteLink" />
</div>
<QRcode
v-model:show="showQRcode"
mode="invitation"
:linkIdentifier="linkIdentifier"
/>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { aesEncrypt } from "@/utils/crypto";
import { useAgentStore } from "@/stores/agentStore";
const agentStore = useAgentStore();
const { mobile, agentID } = storeToRefs(agentStore); // 响应式解构
import { watch, onMounted } from "vue";
import { generateInviteCode, getInviteCodeList, deleteInviteCode, getInviteLink } from "@/api/agent";
import { showToast, showConfirmDialog } from "vant";
import QRcode from "@/components/QRcode.vue";
const showQRcode = ref(false);
const linkIdentifier = ref("");
onBeforeMount(() => {
encryptIdentifire(agentID.value, mobile.value);
const inviteLink = ref("");
const showInviteLink = ref(false); // 控制是否显示邀请链接区域
const generatedCodes = ref([]);
const showGenerateCodeDialog = ref(false);
const generateCount = ref(1);
const expireDays = ref(0);
const remark = ref("");
// 邀请码列表相关
const codeList = ref([]);
const loadingCodeList = ref(false);
const codeListTotal = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const selectedCodeId = ref(null); // 当前选中的邀请码ID
// 页面加载时自动加载邀请码列表
onMounted(() => {
loadCodeList(1);
});
const encryptIdentifire = (agentID, mobile) => {
const linkIdentifierJSON = {
agentID,
mobile,
};
const linkIdentifierStr = JSON.stringify(linkIdentifierJSON);
const encodeData = aesEncrypt(
linkIdentifierStr,
"8e3e7a2f60edb49221e953b9c029ed10"
);
linkIdentifier.value = encodeURIComponent(encodeData);
// 选择邀请码
const selectCode = (item) => {
selectedCodeId.value = item.id;
// 清除之前生成的链接和显示状态
inviteLink.value = "";
showInviteLink.value = false;
};
// 生成邀请链接
const handleGenerateLink = async (code) => {
if (!code) {
showToast({ message: "请先选择邀请码" });
return;
}
try {
// 构建目标路径:注册页面路径
const targetPath = `/register?invite_code=${code}`;
// 调用后端API生成短链
const { data, error } = await getInviteLink({
invite_code: code,
target_path: targetPath
});
if (data.value && !error.value && data.value.code === 200) {
inviteLink.value = data.value.data.invite_link || "";
showInviteLink.value = true; // 显示邀请链接区域
// 自动复制到剪贴板
copyToClipboard(inviteLink.value, "邀请链接已生成并复制到剪贴板");
} else {
showToast({ message: data.value?.msg || "生成邀请链接失败,请重试" });
}
} catch (err) {
console.error("生成邀请链接失败:", err);
showToast({ message: "生成邀请链接失败,请重试" });
}
};
// 生成二维码
const handleGenerateQRCode = async (code) => {
if (!code) {
showToast({ message: "请先选择邀请码" });
return;
}
try {
// 构建目标路径:注册页面路径
const targetPath = `/register?invite_code=${code}`;
// 调用后端API生成短链
const { data, error } = await getInviteLink({
invite_code: code,
target_path: targetPath
});
if (data.value && !error.value && data.value.code === 200) {
inviteLink.value = data.value.data.invite_link || "";
// 不设置 showInviteLink这样不会显示邀请链接区域只用于生成二维码
// 直接显示二维码弹窗
showQRcode.value = true;
} else {
showToast({ message: data.value?.msg || "生成邀请链接失败,请重试" });
}
} catch (err) {
console.error("生成邀请链接失败:", err);
showToast({ message: "生成邀请链接失败,请重试" });
}
};
// 生成邀请码
const handleGenerateCode = async () => {
try {
const { data, error } = await generateInviteCode({
count: generateCount.value,
expire_days: expireDays.value || 0,
remark: remark.value || ""
});
if (data.value && !error.value && data.value.code === 200) {
generatedCodes.value = data.value.data.codes || [];
showGenerateCodeDialog.value = false;
showToast({ message: `成功生成 ${generatedCodes.value.length} 个邀请码` });
// 重置表单
generateCount.value = 1;
expireDays.value = 0;
remark.value = "";
// 刷新邀请码列表(会自动选择第一个)
await loadCodeList(1);
} else {
showToast({ message: data.value?.msg || "生成邀请码失败,请重试" });
}
} catch (error) {
console.error("Failed to generate invite code", error);
showToast({ message: "生成邀请码失败,请重试" });
}
};
// 判断邀请码是否过期
const isExpired = (item) => {
if (!item.expire_time) {
// 没有过期时间,表示不过期
return false;
}
// 将过期时间字符串转换为日期对象进行比较
const expireDate = new Date(item.expire_time);
const now = new Date();
return expireDate < now;
};
// 加载邀请码列表
const loadCodeList = async (page = 1) => {
loadingCodeList.value = true;
try {
const { data, error } = await getInviteCodeList({
page: page,
page_size: pageSize.value
});
if (data.value && !error.value && data.value.code === 200) {
codeList.value = data.value.data.list || [];
codeListTotal.value = data.value.data.total || 0;
currentPage.value = page;
// 如果有邀请码,默认选择第一个
if (codeList.value.length > 0 && !selectedCodeId.value) {
selectedCodeId.value = codeList.value[0].id;
}
} else {
showToast({ message: data.value?.msg || "获取邀请码列表失败" });
}
} catch (error) {
console.error("Failed to load invite code list", error);
showToast({ message: "获取邀请码列表失败" });
} finally {
loadingCodeList.value = false;
}
};
// 删除邀请码
const handleDeleteCode = async (id, code) => {
try {
await showConfirmDialog({
title: "确认删除",
message: `确定要删除邀请码 ${code} 吗?此操作不可恢复。`
});
const { data, error } = await deleteInviteCode({ id });
if (data.value && !error.value && data.value.code === 200) {
showToast({ message: "删除成功" });
// 如果删除的是当前选中的,清除选中状态
if (selectedCodeId.value === id) {
selectedCodeId.value = null;
inviteLink.value = "";
}
// 刷新列表
await loadCodeList(currentPage.value);
// 如果还有邀请码,自动选择第一个
if (codeList.value.length > 0 && !selectedCodeId.value) {
selectedCodeId.value = codeList.value[0].id;
}
} else {
showToast({ message: data.value?.msg || "删除失败,请重试" });
}
} catch (error) {
if (error !== "cancel") {
console.error("Failed to delete invite code", error);
showToast({ message: "删除失败,请重试" });
}
}
};
// 复制邀请码
const copyInviteCode = (code) => {
if (!code) {
showToast({ message: "邀请码不能为空" });
return;
}
copyToClipboard(code);
};
// 复制邀请链接
const copyInviteLink = () => {
if (!inviteLink.value) {
showToast({ message: "请先生成邀请链接" });
return;
}
copyToClipboard(inviteLink.value);
};
// 复制到剪贴板
const copyToClipboard = (text, customMessage = "已复制到剪贴板") => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(text)
.then(() => {
showToast({ message: customMessage });
})
.catch((err) => {
console.error("复制失败:", err);
fallbackCopy(text, customMessage);
});
} else {
fallbackCopy(text, customMessage);
}
};
// 降级复制方法
const fallbackCopy = (text, customMessage = "已复制到剪贴板") => {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
showToast({ message: customMessage });
} catch (err) {
console.error("复制失败:", err);
showToast({ message: "复制失败,请手动复制" });
} finally {
document.body.removeChild(textArea);
}
};
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
// 可以添加一些自定义样式</style>

View File

@@ -3,20 +3,9 @@
<img src="@/assets/images/invitation_agent_apply.png" alt="邀请代理申请" />
<!-- 统一状态处理容器 -->
<div class="flex flex-col items-center justify-centerx">
<!-- 审核中状态 -->
<div v-if="displayStatus === 0" class="text-center">
<span class="text-xs text-gray-500">的申请正在审核中</span>
<div class="bg-gray-200 p-1 rounded-3xl shadow-xl mt-1">
<div
class="text-xl font-bold px-8 py-2 bg-gray-400 text-white rounded-3xl shadow-lg cursor-not-allowed">
审核进行中
</div>
</div>
</div>
<!-- 审核通过状态 -->
<div v-if="displayStatus === 1" class="text-center">
<span class="text-xs text-gray-500">您已成为认证代理方</span>
<!-- 已是代理状态 -->
<div v-if="isAgent" class="text-center">
<span class="text-xs text-gray-500">已是代理欢迎回来</span>
<div class="bg-green-100 p-1 rounded-3xl shadow-xl mt-1" @click="goToHome">
<div
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-green-500 to-green-300 text-white rounded-3xl shadow-lg cursor-pointer">
@@ -25,19 +14,8 @@
</div>
</div>
<!-- 审核未通过状态 -->
<div v-if="displayStatus === 2" class="text-center">
<span class="text-xs text-red-500">审核未通过请重新提交</span>
<div class="bg-red-100 p-1 rounded-3xl shadow-xl mt-1" @click="agentApply">
<div
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-red-500 to-red-300 text-white rounded-3xl shadow-lg cursor-pointer">
重新提交申请
</div>
</div>
</div>
<!-- 未申请状态包含邀请状态 -->
<div v-if="displayStatus === 3" class="text-center">
<!-- 未成为代理状态包含邀请状态 -->
<div v-else class="text-center">
<span class="text-xs text-gray-500">{{
isSelf ? "立即申请成为代理人" : "邀您注册代理人"
}}</span>
@@ -50,8 +28,7 @@
</div>
</div>
</div>
<AgentApplicationForm v-model:show="showApplyPopup" @submit="submitApplication" @close="showApplyPopup = false"
:ancestor="ancestor" :is-self="isSelf" :user-name="userName" />
<AgentApplicationForm v-model:show="showApplyPopup" @submit="submitApplication" @close="showApplyPopup = false" />
</template>
<script setup>
@@ -64,63 +41,66 @@ import { ref } from "vue";
const store = useAgentStore();
const userStore = useUserStore();
const { userName } = storeToRefs(userStore);
const { status } = storeToRefs(store); // 响应式解构
const { isAgent } = storeToRefs(store);
const ancestor = ref("");
const isSelf = ref(false);
const agentApply = () => {
showApplyPopup.value = true;
};
// 计算显示状态当isSelf为false时强制显示为3
const displayStatus = computed(() => {
// return isSelf.value ? status.value : 3;
return status.value;
});
// 跳转到首页
const goToHome = () => {
clearInterval(intervalId);
router.replace("/promote");
};
onBeforeMount(() => {
if (route.name === "invitationAgentApplySelf") {
isSelf.value = true;
onBeforeMount(async () => {
// 如果是通过邀请链接访问旧的linkIdentifier格式提取信息
if (route.params.linkIdentifier && route.name !== "invitationAgentApplySelf") {
try {
const linkIdentifier = route.params.linkIdentifier;
const decryptDataStr = aesDecrypt(
decodeURIComponent(linkIdentifier),
"8e3e7a2f60edb49221e953b9c029ed10"
);
const decryptData = JSON.parse(decryptDataStr);
// 旧格式可能包含agentID和mobile但新系统使用邀请码
// 这里可以保留兼容,但主要功能是通过邀请码
} catch (error) {
console.error("解析链接标识符失败", error);
}
} else {
const linkIdentifier = route.params.linkIdentifier;
const decryptDataStr = aesDecrypt(
decodeURIComponent(linkIdentifier),
"8e3e7a2f60edb49221e953b9c029ed10"
);
const decryptData = JSON.parse(decryptDataStr);
ancestor.value = decryptData.mobile;
isSelf.value = true;
}
// 检查是否已登录并获取代理状态
const token = localStorage.getItem("token");
if (token) {
store.fetchAgentStatus();
await store.fetchAgentStatus();
}
});
import { applyForAgent, registerByInviteCode } from "@/api/agent";
const submitApplication = async (formData) => {
// 提交代理申请的数据
const { region, mobile, wechat_id, code } = formData;
const { region, mobile, wechat_id, code, referrer } = formData;
// 根据是否已登录选择不同的API
const isLoggedIn = !!localStorage.getItem("token");
const apiCall = isLoggedIn ? applyForAgent : registerByInviteCode;
let postData = {
region,
mobile,
wechat_id,
code,
referrer,
};
if (!isSelf.value) {
postData.ancestor = ancestor.value;
}
const { data, error } = await useApiFetch("/agent/apply")
.post(postData)
.json();
const { data, error } = await apiCall(postData);
if (data.value && !error.value) {
if (data.value.code === 200) {
showApplyPopup.value = false;
showToast({ message: "已提交申请" });
// refreshAgentStatus()
showToast({ message: "注册成功,您已成为代理!" });
// 更新token和状态
if (data.value.data.accessToken) {
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem(
@@ -131,38 +111,17 @@ const submitApplication = async (formData) => {
"accessExpire",
data.value.data.accessExpire
);
refreshAgentStatus();
// 重新获取代理状态
await store.fetchAgentStatus();
await userStore.fetchUserInfo();
// 跳转到代理主页
router.replace("/agent");
}
} else {
console.log("申请失败", data.value);
}
}
};
let intervalId = null; // 保存定时器 ID
const refreshAgentStatus = () => {
// 当 status.value 变化时(如通过监听或事件)
if (status.value === 3) {
// 清除已有定时器(避免重复创建)
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
// 每次执行前检查状态是否仍为 3
if (status.value !== 3) {
clearInterval(intervalId);
intervalId = null;
return;
}
store.fetchAgentStatus();
}, 2000);
} else {
// 状态不是 3 时主动清除定时器
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="p-4">
<div class="mb-4 flex items-center justify-between">
<div>
<div class="text-sm" style="color: var(--van-text-color-2);">我的邀请码</div>
<div class="text-2xl font-bold" style="color: var(--van-theme-primary);">{{ agentCode }}</div>
</div>
<button @click="copyInviteCode"
class="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm font-medium active:bg-blue-600 shadow-sm">复制邀请码</button>
</div>
<div class="p-3">
<QRcode :inviteLink="inviteLink" mode="invitation" :asPage="true" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { showToast } from "vant";
import { storeToRefs } from "pinia";
import QRcode from "@/components/QRcode.vue";
import { useAgentStore } from "@/stores/agentStore";
import { getInviteLink } from "@/api/agent";
const inviteLink = ref("");
const agentStore = useAgentStore();
const { agentCode } = storeToRefs(agentStore);
const copyText = async (text, tip) => {
try {
await navigator.clipboard.writeText(text);
showToast({ message: tip || "已复制" });
} catch (e) {
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
showToast({ message: tip || "已复制" });
} finally {
document.body.removeChild(ta);
}
}
};
const copyInviteCode = () => {
if (!agentCode.value) {
showToast({ message: "未获取到邀请码" });
return;
}
copyText(String(agentCode.value), "邀请码已复制");
};
const copyInviteLink = () => {
if (!inviteLink.value) {
showToast({ message: "暂无邀请链接" });
return;
}
copyText(inviteLink.value, "邀请链接已复制");
};
onMounted(async () => {
if (!agentStore.isLoaded) {
await agentStore.fetchAgentStatus();
}
if (!agentStore.isAgent || !agentCode.value) {
showToast({ message: "请注册成为代理后再邀请" });
return;
}
const targetPath = `/register?invite_code=${agentCode.value}`;
const { data, error } = await getInviteLink({ invite_code: agentCode.value, target_path: targetPath });
if (data.value && !error.value && data.value.code === 200) {
inviteLink.value = data.value.data.invite_link || "";
} else {
showToast({ message: data.value?.msg || "获取邀请链接失败" });
}
});
</script>
<style scoped></style>

View File

@@ -1,39 +1,31 @@
<script setup>
import { ref, computed, onUnmounted, nextTick } from 'vue'
import { showToast } from 'vant'
import ClickCaptcha from '@/components/ClickCaptcha.vue'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { useRouter, useRoute } from 'vue-router'
import { mobileCodeLogin } from '@/api/user'
import useApiFetch from '@/composables/useApiFetch'
const router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const phoneNumber = ref('')
const verificationCode = ref('')
const password = ref('')
const isPasswordLogin = ref(false)
const isAgreed = ref(false)
const isCountingDown = ref(false)
const countdown = ref(60)
let timer = null
// 验证组件状态
const showCaptcha = ref(false)
const captchaVerified = ref(false)
// 聚焦状态变量
const phoneFocused = ref(false)
const codeFocused = ref(false)
const passwordFocused = ref(false)
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value)
})
const canLogin = computed(() => {
if (!isPhoneNumberValid.value) return false
if (isPasswordLogin.value) {
return password.value.length >= 6
} else {
return verificationCode.value.length === 6
}
return isPhoneNumberValid.value && verificationCode.value.length === 6
})
const hideRegister = computed(() => route.query.from === 'promotionInquire')
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return
@@ -41,6 +33,7 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" });
return
}
const { data, error } = await useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'login' })
.json()
@@ -80,74 +73,63 @@ async function handleLogin() {
showToast({ message: "请输入有效的手机号" });
return
}
if (isPasswordLogin.value) {
if (password.value.length < 6) {
showToast({ message: "密码长度不能小于6位" });
return
}
} else {
if (verificationCode.value.length !== 6) {
showToast({ message: "请输入有效的验证码" });
return
}
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" });
if (verificationCode.value.length !== 6) {
showToast({ message: "请输入有效的验证码" });
return
}
// 显示验证组件
showCaptcha.value = true
}
// 验证成功回调
function handleCaptchaSuccess() {
captchaVerified.value = true
showCaptcha.value = false
// 执行实际的登录逻辑
performLogin()
}
try {
const { data, error } = await mobileCodeLogin({
mobile: phoneNumber.value,
code: verificationCode.value
})
// 验证关闭回调
function handleCaptchaClose() {
showCaptcha.value = false
}
if (data.value && !error.value) {
if (data.value.code === 200) {
// 保存token
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
// 执行实际的登录逻辑
async function performLogin() {
const { data, error } = await useApiFetch('/user/mobileCodeLogin')
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json()
// 获取用户信息和代理信息
await Promise.all([
userStore.fetchUserInfo(),
agentStore.fetchAgentStatus()
])
if (data.value && !error.value) {
if (data.value.code === 200) {
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
window.location.href = '/'
showToast({ message: "登录成功!" });
setTimeout(() => {
const redirect = route.query.redirect ? decodeURIComponent(route.query.redirect) : '/'
router.replace(redirect)
}, 500)
} else {
showToast(data.value.msg || "登录失败,请重试")
}
}
} catch (err) {
console.error('登录失败:', err)
showToast({ message: "登录失败,请重试" });
}
}
function toUserAgreement() {
router.push(`/userAgreement`)
}
function toPrivacyPolicy() {
router.push(`/privacyPolicy`)
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
const onClickLeft = () => {
router.replace('/')
router.back()
}
const goToRegister = () => {
router.push('/register')
}
</script>
<template>
<div class="login-layout ">
<van-nav-bar fixed placeholder title="用户登录" left-text="" left-arrow @click-left="onClickLeft" />
<van-nav-bar fixed placeholder title="登录" left-text="" left-arrow @click-left="onClickLeft" />
<div class="login px-4 relative z-10">
<div class="mb-8 pt-20 text-left">
<div class="flex flex-col items-center">
@@ -177,32 +159,17 @@ const onClickLeft = () => {
</div>
</div>
<!-- 协议同意框 -->
<div class="agreement-wrapper">
<input type="checkbox" v-model="isAgreed" class="agreement-checkbox accent-primary"
id="agreement" />
<label for="agreement" class="agreement-text">
我已阅读并同意
<a class="agreement-link" @click="toUserAgreement">用户协议</a>
<a class="agreement-link" @click="toPrivacyPolicy">隐私政策</a>
</label>
</div>
<!-- 提示文字 -->
<div class="notice-text">
未注册手机号登录后将自动生成账号并且代表您已阅读并同意
</div>
<!-- 登录按钮 -->
<button class="login-btn" :class="{ 'disabled': !canLogin }" @click="handleLogin" :disabled="!canLogin">
登录
</button>
<!-- 注册按钮推广链接来源的登录不展示 -->
<button v-if="!hideRegister" class="register-btn" @click="goToRegister">
注册成为代理
</button>
</div>
</div>
<!-- 点击验证组件 -->
<ClickCaptcha :visible="showCaptcha" @success="handleCaptchaSuccess" @close="handleCaptchaClose" />
</div>
</template>
@@ -217,8 +184,6 @@ const onClickLeft = () => {
overflow: hidden;
}
.login {}
/* 登录表单 */
.login-form {
background-color: var(--color-bg-primary);
@@ -295,37 +260,25 @@ const onClickLeft = () => {
cursor: not-allowed;
}
/* 协议同意 */
.agreement-wrapper {
display: flex;
align-items: center;
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.agreement-checkbox {
flex-shrink: 0;
margin-right: 0.5rem;
}
.agreement-text {
font-size: 0.75rem;
color: var(--color-text-secondary);
line-height: 1.4;
}
.agreement-link {
/* 注册按钮 */
.register-btn {
width: 100%;
padding: 0.875rem;
background-color: transparent;
color: var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.3s;
letter-spacing: 0.25rem;
margin-top: 1rem;
}
/* 提示文字 */
.notice-text {
font-size: 0.6875rem;
color: var(--color-text-tertiary);
line-height: 1.5;
margin-bottom: 2rem;
.register-btn:hover {
background-color: var(--color-primary);
color: var(--color-text-white);
}
/* 登录按钮 */

View File

@@ -1,221 +1,326 @@
<template>
<div class="box-border min-h-screen">
<div class="flex flex-col p-4 space-y-6">
<!-- 用户信息卡片 -->
<div class="profile-section group relative flex items-center gap-4 rounded-xl bg-white p-6 transition-all hover:shadow-xl"
<!-- 用户信息和资产卡片合并 -->
<div class="profile-section group relative rounded-xl p-0.5 transition-all hover:shadow-xl"
:class="isAgent ? levelGradient.cardBorder : 'bg-gray-200'"
@click="!isLoggedIn ? redirectToLogin() : null">
<div class="relative">
<!-- 头像容器添加overflow-hidden解决边框问题 -->
<div class="overflow-hidden rounded-full p-0.5" :class="levelGradient.border">
<img :src="userAvatar || getDefaultAvatar()" alt="User Avatar"
class="h-24 w-24 rounded-full border-4 border-white" />
</div>
<!-- 代理标识 -->
<div v-if="isAgent" class="absolute -bottom-2 -right-2">
<div class="flex items-center justify-center rounded-full px-3 py-1 text-xs font-bold text-white shadow-sm"
:class="levelGradient.badge">
{{ levelNames[level] }}
</div>
</div>
</div>
<div class="space-y-1">
<h2 class="text-2xl font-bold" style="color: var(--van-text-color);">
{{
!isLoggedIn
? "点击登录"
: mobile
? maskName(mobile)
: isWeChat
? "微信用户"
: "未绑定手机号"
}}
</h2>
<!-- 手机号绑定提示 -->
<template v-if="isLoggedIn && !mobile">
<p @click.stop="showBindPhoneDialog" class="text-sm cursor-pointer hover:underline"
style="color: var(--van-theme-primary);">
点击绑定手机号码
</p>
</template>
<p v-if="isAgent" class="text-sm font-medium" :class="levelGradient.text">
🎖 {{ levelText[level] }}
</p>
</div>
</div>
<VipBanner v-if="isAgent && (level === 'normal' || level === '')" />
<!-- 功能菜单 -->
<div class="">
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<template v-if="isAgent && ['VIP', 'SVIP'].includes(level)">
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-purple-50 transition-colors border-b border-gray-100"
@click="toVipConfig">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/dlbgpz.png" class="w-6 h-6 object-contain" alt="代理报告配置" />
<span class="text-purple-700 font-medium">代理报告配置</span>
<div class="rounded-xl bg-white p-6">
<!-- 上半部分用户信息 -->
<div class="flex items-center gap-4 mb-4">
<div class="relative">
<!-- 头像容器添加overflow-hidden解决边框问题 -->
<div class="overflow-hidden rounded-full p-0.5" :class="levelGradient.border">
<img :src="userAvatar || getDefaultAvatar()" alt="User Avatar"
class="h-20 w-20 rounded-full border-4 border-white" />
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-amber-50 transition-colors border-b border-gray-100"
@click="toVipRenewal">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/xfhy.png" class="w-6 h-6 object-contain" alt="代理会员" />
<div class="flex flex-col items-start">
<span class="text-amber-700 font-medium">续费代理会员</span>
<span class="text-xs text-gray-500">有效期至 {{ formatExpiryTime(ExpiryTime) }}</span>
<!-- 代理标识 -->
<div v-if="isAgent" class="absolute -bottom-2 -right-2">
<div class="flex items-center justify-center rounded-full px-3 py-1 text-xs font-bold text-white shadow-sm"
:class="levelGradient.badge">
{{ levelNamesMap[level] || levelNamesMap[1] }}
</div>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
</div>
<div class="flex-1 space-y-1">
<h2 class="text-2xl font-bold" style="color: var(--van-text-color);">
{{
!isLoggedIn
? "点击登录"
: mobile
? mobile
: isWeChat
? "微信用户"
: "未绑定手机号"
}}
</h2>
<!-- 手机号绑定提示 -->
<template v-if="isLoggedIn && !mobile">
<p @click.stop="showRegisterAgentDialog" class="text-sm cursor-pointer hover:underline"
style="color: var(--van-theme-primary);">
点击注册成为代理
</p>
</template>
<!-- 普通用户申请成为代理提示 -->
<template v-else-if="isLoggedIn && mobile && !isAgent">
<p @click.stop="toRegister" class="text-sm cursor-pointer hover:underline"
style="color: var(--van-theme-primary);">
点击申请成为代理
</p>
</template>
<p v-if="isAgent" class="font-bold" :class="levelGradient.text">
ID: {{ agentCode }}
</p>
</div>
<!-- 右侧资产信息仅代理显示 -->
<div v-if="isAgent" class="text-right">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">余额</div>
<div class="text-2xl font-bold" style="color: var(--van-theme-primary);">
¥ {{ (revenueData?.balance || 0).toFixed(2) }}
</div>
</div>
</div>
<!-- 下半部分资产详情和操作按钮仅代理显示 -->
<template v-if="isAgent">
<div class="grid grid-cols-3 gap-3 mb-4 pt-4 border-t"
style="border-color: var(--van-border-color);">
<div class="text-center">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">累计收益</div>
<div class="text-base font-semibold" style="color: var(--van-text-color);">
¥ {{ (revenueData?.total_earnings || 0).toFixed(2) }}
</div>
</div>
<div class="text-center">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">风险保障金</div>
<div class="text-base font-semibold" style="color: var(--van-text-color);">
¥ {{ (revenueData?.frozen_balance || 0).toFixed(2) }}
</div>
</div>
<div class="text-center">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">累计提现</div>
<div class="text-base font-semibold" style="color: var(--van-text-color);">
¥ {{ (revenueData?.withdrawn_amount || 0).toFixed(2) }}
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<button @click.stop="toWithdraw"
class="text-white rounded-full py-1.5 px-4 shadow-md flex items-center justify-center text-base"
style="background: linear-gradient(135deg, var(--van-theme-primary), var(--van-theme-primary-dark)); min-height: 36px;">
<van-icon name="gold-coin" class="mr-1" />
提现
</button>
<button @click.stop="toWithdrawDetails"
class="bg-white/90 border rounded-full py-1.5 px-4 shadow-sm flex items-center justify-center text-base"
style="color: var(--van-text-color-2); border-color: var(--van-border-color); min-height: 36px;">
<van-icon name="notes" class="mr-1" />
提现记录
</button>
</div>
</template>
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors border-b border-gray-100"
@click="toHistory">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/wdbg.png" class="w-6 h-6 object-contain" alt="我的报告" />
<span class="text-gray-700 font-medium">我的报告</span>
</div>
</div>
<!-- 佣金和下级推广收益合并 -->
<div v-if="isAgent" class="rounded-xl shadow-lg bg-white p-5">
<div class="grid grid-cols-2 gap-4">
<!-- 我的推广收益部分 -->
<div class="pr-4 border-r" style="border-color: var(--van-border-color);">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<van-icon name="balance-list" class="text-lg mr-2"
style="color: var(--color-warning);" />
<span class="text-base font-bold" style="color: var(--van-text-color);">我的推广收益</span>
</div>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors border-b border-gray-100"
@click="toApiStore">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/apisd.png" class="w-6 h-6 object-contain" alt="API商店" />
<span class="text-gray-700 font-medium">API商店</span>
<div class="text-center mb-3">
<div class="text-xl font-bold" style="color: var(--color-warning);">
¥ {{ (revenueData?.commission_total || 0).toFixed(2) }}
</div>
<div class="text-sm mt-0.5" style="color: var(--van-text-color-2);">累计总收益</div>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
<!-- <button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors border-b border-gray-100"
@click="toBigData">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/qyfkyj.png" class="w-6 h-6 object-contain" alt="企业风控预警" />
<span class="text-gray-700 font-medium">企业风控预警</span>
<div class="grid grid-cols-2 gap-2 mb-3">
<div class="text-center">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">今日</div>
<div class="text-base font-semibold" style="color: var(--color-warning);">
¥ {{ (revenueData?.commission_today || 0).toFixed(2) }}
</div>
</div>
<div class="text-center">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">本月</div>
<div class="text-base font-semibold" style="color: var(--color-warning);">
¥ {{ (revenueData?.commission_month || 0).toFixed(2) }}
</div>
</div>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> -->
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors border-b border-gray-100"
@click="toCooperation">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/swhz.png" class="w-6 h-6 object-contain" alt="商务合作" />
<span class="text-gray-700 font-medium">商务合作</span>
<div class="flex items-center justify-center text-sm font-semibold cursor-pointer"
style="color: var(--color-warning);" @click="goToPromoteDetail">
<span>查看明细</span>
<span class="ml-1 text-base"></span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors border-b border-gray-100"
@click="toUserAgreement">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/yhxy.png" class="w-6 h-6 object-contain" alt="用户协议" />
<span class="text-gray-700 font-medium">用户协议</span>
</div>
<!-- 下级推广收益部分 -->
<div class="pl-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<van-icon name="gift-o" class="text-lg mr-2" style="color: var(--color-success);" />
<span class="text-base font-bold" style="color: var(--van-text-color);">下级推广收益</span>
</div>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors border-b border-gray-100"
@click="toPrivacyPolicy">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/yszc.png" class="w-6 h-6 object-contain" alt="隐私政策" />
<span class="text-gray-700 font-medium">隐私政策</span>
<div class="text-center mb-3">
<div class="text-xl font-bold" style="color: var(--color-success);">
¥ {{ (revenueData?.rebate_total || 0).toFixed(2) }}
</div>
<div class="text-sm mt-0.5" style="color: var(--van-text-color-2);">累计总收益</div>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
<button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors"
@click="toService">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/lxkf.png" class="w-6 h-6 object-contain" alt="联系客服" />
<span class="text-gray-700 font-medium">联系客服</span>
<div class="grid grid-cols-2 gap-2 mb-3">
<div class="text-center">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">今日</div>
<div class="text-base font-semibold" style="color: var(--color-success);">
¥ {{ (revenueData?.rebate_today || 0).toFixed(2) }}
</div>
</div>
<div class="text-center">
<div class="text-sm mb-1" style="color: var(--van-text-color-2);">本月</div>
<div class="text-base font-semibold" style="color: var(--color-success);">
¥ {{ (revenueData?.rebate_month || 0).toFixed(2) }}
</div>
</div>
</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">
<div class="flex items-center gap-3">
<img src="@/assets/images/me/tcdl.png" class="w-6 h-6 object-contain" alt="退出登录" />
<span class="text-gray-700 font-medium">退出登录</span>
<div class="flex items-center justify-center text-sm font-semibold cursor-pointer"
style="color: var(--color-success);" @click="goToRebateDetail">
<span>查看明细</span>
<span class="ml-1 text-base"></span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button>
</div>
</div>
</div>
<!-- 功能菜单 -->
<div class="">
<div class="bg-white rounded-xl shadow-sm p-4">
<div class="grid grid-cols-4 gap-4">
<!-- 升级功能入口如果不是钻石代理 -->
<button v-if="isAgent && level !== 3"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-purple-50 transition-colors"
@click="toUpgrade">
<img src="@/assets/images/me/sjdl.svg" class="w-8 h-8 object-contain" alt="升级代理" />
<span class="text-xs text-gray-700 font-medium text-center">升级代理</span>
</button>
<!-- 升级下级入口仅钻石代理 -->
<button v-if="isAgent && level === 3"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-purple-50 transition-colors"
@click="toUpgradeSubordinate">
<img src="@/assets/images/me/sjxj.svg" class="w-8 h-8 object-contain" alt="调整下级级别" />
<span class="text-xs text-gray-700 font-medium text-center">调整级别</span>
</button>
<!-- 邀请码管理入口 -->
<button v-if="isAgent"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toInviteCodeManage">
<img src="@/assets/images/me/yqmgl.svg" class="w-8 h-8 object-contain" alt="邀请码管理" />
<span class="text-xs text-gray-700 font-medium text-center">邀请码管理</span>
</button>
<!-- 实名认证入口所有代理 -->
<button v-if="isAgent"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-green-50 transition-colors"
@click="toRealNameAuth">
<img src="@/assets/images/me/smrz.svg" class="w-8 h-8 object-contain" alt="提现" />
<span class="text-xs text-gray-700 font-medium text-center">实名认证</span>
</button>
<!-- 提现入口所有代理 -->
<!-- <button v-if="isAgent"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toWithdraw">
<img src="@/assets/images/me/tx.svg" class="w-8 h-8 object-contain" alt="提现" />
<span class="text-xs text-gray-700 font-medium text-center">提现</span>
</button> -->
<!-- 推广查询记录 -->
<button v-if="isAgent"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toAgentReport">
<img src="@/assets/images/me/tgcxjl.svg" class="w-8 h-8 object-contain" alt="推广查询记录" />
<span class="text-xs text-gray-700 font-medium text-center">推广查询记录</span>
</button>
<button
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toUserAgreement">
<img src="@/assets/images/me/yhxy.svg" class="w-8 h-8 object-contain" alt="用户协议" />
<span class="text-xs text-gray-700 font-medium text-center">用户协议</span>
</button>
<button
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toPrivacyPolicy">
<img src="@/assets/images/me/yszc.svg" class="w-8 h-8 object-contain" alt="隐私政策" />
<span class="text-xs text-gray-700 font-medium text-center">隐私政策</span>
</button>
<button
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toService">
<img src="@/assets/images/me/lxkf.svg" class="w-8 h-8 object-contain" alt="联系客服" />
<span class="text-xs text-gray-700 font-medium text-center">联系客服</span>
</button>
<button v-if="isLoggedIn && !isWeChat"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-red-50 transition-colors"
@click="handleLogout">
<img src="@/assets/images/me/tcdl.png" class="w-8 h-8 object-contain" alt="退出登录" />
<span class="text-xs text-gray-700 font-medium text-center">退出登录</span>
</button>
</div>
</div>
</div>
</div>
<BindPhoneDialog />
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { ref, computed, onBeforeMount } from "vue";
const router = useRouter();
import { ref, computed, onBeforeMount, onMounted } from "vue";
import { useRouter } from "vue-router";
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";
import { getRevenueInfo } from '@/api/agent';
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
const router = useRouter();
const agentStore = useAgentStore();
const userStore = useUserStore();
const dialogStore = useDialogStore();
const { isAgent, level, ExpiryTime } = storeToRefs(agentStore);
const { isAgent, level, levelName, agentCode } = storeToRefs(agentStore);
const { userName, userAvatar, isLoggedIn, mobile } = storeToRefs(userStore);
const { isWeChat } = useEnv();
const revenueData = ref(null);
const levelNames = {
normal: "普通代理",
"": "普通代理",
VIP: "VIP代理",
SVIP: "SVIP代理",
// 等级名称映射(数字等级)
const levelNamesMap = {
1: "普通代理",
2: "黄金代理",
3: "钻石代理",
};
const levelText = {
normal: "基础代理特权",
"": "基础代理特权",
VIP: "高级代理特权",
SVIP: "尊享代理特权",
const levelTextMap = {
1: "基础代理特权",
2: "高级代理特权",
3: "尊享代理特权",
};
const levelGradient = computed(() => ({
border: {
normal: "bg-gradient-to-r from-gray-300 to-gray-400",
"": "bg-gradient-to-r from-gray-300 to-gray-400",
VIP: "bg-gradient-to-r from-yellow-400 to-amber-500",
SVIP: "bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_15px_rgba(163,51,200,0.2)]",
}[level.value],
badge: {
normal: "bg-gradient-to-r from-gray-500 to-gray-600",
"": "bg-gradient-to-r from-gray-500 to-gray-600",
VIP: "bg-gradient-to-r from-yellow-500 to-amber-600",
SVIP: "bg-gradient-to-r from-purple-500 to-pink-500",
}[level.value],
text: {
normal: "text-gray-600",
"": "text-gray-600",
VIP: "text-amber-600",
SVIP: "text-purple-600",
}[level.value],
}));
const levelGradient = computed(() => {
const currentLevel = level.value || 1;
const gradients = {
1: {
border: "bg-gradient-to-r from-gray-300 to-gray-400",
badge: "bg-gradient-to-r from-gray-500 to-gray-600",
text: "text-gray-600",
cardBorder: "bg-gradient-to-r from-gray-300 to-gray-400",
},
2: {
border: "bg-gradient-to-r from-yellow-400 to-amber-500",
badge: "bg-gradient-to-r from-yellow-500 to-amber-600",
text: "text-amber-600",
cardBorder: "bg-gradient-to-r from-yellow-400 to-amber-500",
},
3: {
border: "bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_15px_rgba(163,51,200,0.2)]",
badge: "bg-gradient-to-r from-purple-500 to-pink-500",
text: "text-purple-600",
cardBorder: "bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_20px_rgba(163,51,200,0.3)]",
},
};
return gradients[currentLevel] || gradients[1];
});
const maskName = (name) => {
if (!name || name.length < 11) return name;
return name.substring(0, 3) + "****" + name.substring(7);
};
function toHistory() {
router.push(`/historyQuery`);
}
function toCooperation() {
window.location.href = "https://www.tianyuandata.com";
}
function toApiStore() {
window.location.href = "https://www.tianyuanapi.com/";
}
function toUserAgreement() {
router.push(`/userAgreement`);
}
@@ -266,42 +371,73 @@ function handleCancelAccount() {
function toService() {
window.location.href =
"https://work.weixin.qq.com/kfid/kfc8a32720024833f57"; // 跳转到客服页面
"https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3"; // 跳转到客服页面
}
const toVipConfig = () => {
router.push({ name: "agentVipConfig" });
const toUpgrade = () => {
router.push({ name: "agentUpgrade" });
};
const toVipRenewal = () => {
router.push(`/agent/vipApply`);
const toUpgradeSubordinate = () => {
router.push({ name: "upgradeSubordinate" });
};
function formatExpiryTime(expiryTimeStr) {
if (!expiryTimeStr) return "未知";
// 假设expiryTimeStr格式是 "YYYY-MM-DD HH:MM:SS"
// 只返回日期部分 "YYYY-MM-DD"
return expiryTimeStr.split(" ")[0];
const toInviteCodeManage = () => {
router.push({ name: "invitation" });
};
const toRealNameAuth = () => {
dialogStore.openRealNameAuth();
};
const toAgentReport = () => {
router.push({ name: "agentPromotionQueryList" })
}
const toWithdraw = () => {
router.push({ name: "withdraw" });
};
const toWithdrawDetails = () => {
router.push({ name: "withdrawDetails" });
};
const goToPromoteDetail = () => {
router.push({ name: "promoteDetails" });
};
const goToRebateDetail = () => {
router.push({ name: "rewardsDetails" });
};
const getRevenueData = async () => {
if (!isAgent.value) return;
const { data: revenueResponse, error: revenueError } = await getRevenueInfo();
if (revenueResponse.value?.code === 200 && !revenueError.value) {
revenueData.value = revenueResponse.value.data;
}
};
const getDefaultAvatar = () => {
if (!isAgent.value) return headShot;
switch (level.value) {
case "normal":
case "":
const currentLevel = level.value || 1;
switch (currentLevel) {
case 1:
return "/image/shot_nonal.png";
case "VIP":
case 2:
return "/image/shot_vip.png";
case "SVIP":
case 3:
return "/image/shot_svip.png";
default:
return headShot;
}
};
const showBindPhoneDialog = () => {
dialogStore.openBindPhone();
const showRegisterAgentDialog = () => {
dialogStore.openRegisterAgent();
};
const toRegister = () => {
router.push("/register");
};
onBeforeMount(() => {
@@ -326,6 +462,12 @@ onBeforeMount(() => {
}
}
});
onMounted(() => {
if (isAgent.value) {
getRevenueData();
}
});
</script>
<style scoped></style>

View File

@@ -29,20 +29,21 @@
<div class="flex justify-between">
<span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}</span>
paymentType === "agent_upgrade"
? "代理升级"
: "查询服务"
}}</span>
</div>
</div>
<div v-if="paymentType === 'agent_vip'" class="text-center text-gray-600 mb-4">恭喜你成为高级代理会员享受更多权益</div>
<div v-if="paymentType === 'agent_upgrade'" class="text-center text-gray-600 mb-4">恭喜你升级代理等级成功享受更多权益
</div>
<div class="action-buttons grid grid-cols-1 gap-4">
<van-button block type="primary" class="rounded-lg" @click="handleNavigation">
{{
paymentType === "agent_vip"
? "查看会员权益"
: "查看查询结果"
paymentType === "agent_upgrade"
? "查看代理信息"
: "查看查询结果"
}}
</van-button>
</div>
@@ -71,10 +72,10 @@
<div class="flex justify-between mb-4">
<span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}</span>
paymentType === "agent_upgrade"
? "代理升级"
: "查询服务"
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">订单状态</span>
@@ -94,7 +95,7 @@
</p>
</div>
<div v-if="paymentType === 'agent_vip'" class="action-buttons grid grid-cols-1 gap-4">
<div v-if="paymentType === 'agent_upgrade'" class="action-buttons grid grid-cols-1 gap-4">
<van-button block type="primary" class="rounded-lg" @click="contactService">
联系客服
</van-button>
@@ -118,9 +119,9 @@
<p class="text-sm mt-1">
已等待
{{
Math.floor(
(pollingCount * getPollingInterval) / 1000
)
Math.floor(
(pollingCount * getPollingInterval) / 1000
)
}}
</p>
@@ -134,16 +135,16 @@
<div v-if="!isApiError" class="flex justify-between mb-4">
<span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{
paymentType === "agent_vip"
? "代理会员"
: "查询服务"
}}</span>
paymentType === "agent_upgrade"
? "代理升级"
: "查询服务"
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">订单状态</span>
<span :class="getStatusTextClass">{{
statusText
}}</span>
}}</span>
</div>
</div>
@@ -408,8 +409,8 @@ onBeforeUnmount(() => {
// 处理导航逻辑
function handleNavigation() {
if (paymentType.value === "agent_vip") {
// 跳转到代理会员页面
if (paymentType.value === "agent_upgrade") {
// 跳转到代理主页
router.replace("/agent");
agentStore.fetchAgentStatus();
userStore.fetchUserInfo();
@@ -431,7 +432,7 @@ function goHome() {
function contactService() {
// 可以替换为实际的客服联系逻辑,如打开聊天窗口或跳转到客服页面
window.location.href =
"https://work.weixin.qq.com/kfid/kfc8a32720024833f57"; // 跳转到客服页面
"https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3"; // 跳转到客服页面
}
// 暴露方法和数据供父组件或路由调用

View File

@@ -14,7 +14,7 @@
</div>
<div class="leading-relaxed">
我们深知个人信息对您的重要性我们将按法律法规要求采取相应安全保护措施尽力保护您的个人信息安全可控
有鉴于此海南省学宇思网络科技有限公司以下简称我们一查查作为一查查产品及服务的提供者制定本隐私政策下称本政策并提醒您
有鉴于此海南海宇大数据有限公司以下简称我们一查查作为一查查产品及服务的提供者制定本隐私政策下称本政策并提醒您
</div>
<div class="leading-relaxed">
本政策适用于全部一查查产品及服务如我们关联公司的产品或服务中使用了一查查提供的产品或服务但未设独立的隐私政策的
@@ -35,7 +35,7 @@
<!-- 第一部分 -->
<div class="leading-relaxed">
<div>
1一查查服务提供者是指研发并提供一查查产品和服务法律主体海南省学宇思网络科技有限公司下称我们一查查
1一查查服务提供者是指研发并提供一查查产品和服务法律主体海南海宇大数据有限公司下称我们一查查
</div>
<div>
2一查查用户是指注册一查查账户的用户以下称
@@ -440,7 +440,7 @@
<div class="leading-relaxed">
隐私政策的解释及争议解决均应适用中华人民共和国大陆地区法律
与本隐私政策相关的任何纠纷双方应协商友好解决若不能协商解决
应将争议提交至海南省学宇思网络科技有限公司注册地有管辖权的人民法院解决
应将争议提交至海南海宇大数据有限公司注册地有管辖权的人民法院解决
</div>
<div class="leading-relaxed">
隐私政策的标题仅为方便及阅读而设并不影响正文其中任何规定的含义或解释

View File

@@ -38,9 +38,14 @@
<!-- 成本和收益信息 -->
<div class="bg-gray-50 rounded-lg p-3 space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">我的成本</span>
<span class="text-sm font-semibold text-orange-500">¥{{ costPrice }}</span>
<span class="text-sm text-gray-600">底价成本</span>
<span class="text-sm font-semibold text-orange-500">¥{{ baseCost }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">提价成本</span>
<span class="text-sm font-semibold text-orange-500">¥{{ raiseCost }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">推广收益</span>
@@ -76,7 +81,7 @@
:product-config="pickerProductConfig" @change="onPriceChange" />
<!-- 二维码弹窗 -->
<QRcode v-model:show="showQRcode" :linkIdentifier="linkIdentifier" />
<QRcode v-model:show="showQRcode" :fullLink="fullLink" />
</div>
</div>
</template>
@@ -87,6 +92,7 @@ import { useRoute, useRouter } from 'vue-router';
import PriceInputPopup from '@/components/PriceInputPopup.vue';
import QRcode from '@/components/QRcode.vue';
import ReportFeatures from '@/components/ReportFeatures.vue';
import { getProductConfig, generateLink } from '@/api/agent';
// 导入logo图片
import personalDataLogo from '@/assets/images/promote/personal_data_logo.png';
@@ -100,16 +106,8 @@ import consumerFinanceReportLogo from '@/assets/images/promote/consumer_finance_
const route = useRoute();
const router = useRouter();
// 报告类型配置(保持原来的数据结构
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: "consumerFinanceReport", id: 28 },
];
// 报告类型配置(从接口获取
const reportTypes = ref([]);
// 从 query 参数获取报告类型(使用 computed 以响应路由变化)
const currentFeature = computed(() => route.query.feature || '');
@@ -121,13 +119,13 @@ const pickerProductConfig = ref(null);
const pickerFieldVal = ref(null); // 保持原来的变量名,用于存储报告类型的 value
const clientPrice = ref(null);
const productConfig = ref(null);
const linkIdentifier = ref("");
const fullLink = ref(""); // 完整的推广短链
const featureData = ref({});
const showQRcode = ref(false);
// Logo映射
const logoMap = {
'personalData': personalDataLogo,
'riskassessment': personalDataLogo,
'companyinfo': companyLogo,
'preloanbackgroundcheck': preloanBackgroundCheckLogo,
'marriage': marriageRiskLogo,
@@ -145,35 +143,41 @@ const currentLogo = computed(() => {
const costPrice = computed(() => {
if (!pickerProductConfig.value) return 0.00
// 确保所有金额值都是数字类型
const baseCost = Number(pickerProductConfig.value.cost_price) || 0;
// 新系统:成本价 = 实际底价actual_base_price
// actual_base_price = base_price + 等级加成
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0;
const clientPriceNum = Number(clientPrice.value) || 0;
const pStandard = Number(pickerProductConfig.value.p_pricing_standard) || 0;
const pRatio = Number(pickerProductConfig.value.p_overpricing_ratio) || 0;
const aStandard = Number(pickerProductConfig.value.a_pricing_standard) || 0;
const aEnd = Number(pickerProductConfig.value.a_pricing_end) || 0;
const aRatio = Number(pickerProductConfig.value.a_overpricing_ratio) || 0;
const priceThreshold = Number(pickerProductConfig.value.price_threshold) || 0;
const priceFeeRate = Number(pickerProductConfig.value.price_fee_rate) || 0;
// 平台定价成本
let platformPricing = baseCost;
// 提价成本计算
if (clientPriceNum > pStandard) {
platformPricing += (clientPriceNum - pStandard) * pRatio;
// 计算提价成本
let priceCost = 0;
if (clientPriceNum > priceThreshold) {
priceCost = (clientPriceNum - priceThreshold) * priceFeeRate;
}
// 代理提价成本计算
if (aStandard > platformPricing && aEnd > platformPricing && aRatio > 0) {
if (clientPriceNum > aStandard) {
if (clientPriceNum > aEnd) {
platformPricing += (aEnd - aStandard) * aRatio;
} else {
platformPricing += (clientPriceNum - aStandard) * aRatio;
}
}
}
// 总成本 = 实际底价 + 提价成本
const totalCost = actualBasePrice + priceCost;
return safeTruncate(platformPricing);
return safeTruncate(totalCost);
});
const baseCost = computed(() => {
if (!pickerProductConfig.value) return "0.00";
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0;
return safeTruncate(actualBasePrice);
});
const raiseCost = computed(() => {
if (!pickerProductConfig.value) return "0.00";
const clientPriceNum = Number(clientPrice.value) || 0;
const priceThreshold = Number(pickerProductConfig.value.price_threshold) || 0;
const priceFeeRate = Number(pickerProductConfig.value.price_fee_rate) || 0;
let priceCost = 0;
if (clientPriceNum > priceThreshold) {
priceCost = (clientPriceNum - priceThreshold) * priceFeeRate;
}
return safeTruncate(priceCost);
});
const promotionRevenue = computed(() => {
@@ -222,10 +226,10 @@ const getProductInfo = async () => {
// 根据 feature 找到对应的报告类型(保持原来的数据结构匹配方式)
const findReportTypeByFeature = (feature) => {
return reportTypes.find(type => type.value === feature);
return reportTypes.value.find(type => type.value === feature);
};
// 选择报告类型并设置配置(保持原来的处理逻辑)
// 选择报告类型并设置配置
const SelectTypePicker = (reportType) => {
if (!reportType) return;
@@ -236,12 +240,12 @@ const SelectTypePicker = (reportType) => {
// 如果产品配置已加载,则设置配置
if (productConfig.value) {
// 遍历产品配置,找到匹配的产品
// 遍历产品配置,找到匹配的产品(根据 product_en 匹配)
for (let i of productConfig.value) {
if (i.product_id === reportType.id) {
if (i.product_en === reportType.value) {
pickerProductConfig.value = i;
// 确保初始价格为数字类型
clientPrice.value = Number(i.cost_price) || 0;
// 新系统:初始价格设置为实际底价(成本价)
clientPrice.value = Number(i.actual_base_price) || Number(i.price_range_min) || 0;
break;
}
}
@@ -256,13 +260,25 @@ const SelectTypePicker = (reportType) => {
// 获取产品配置
const getPromoteConfig = async () => {
const { data, error } = await useApiFetch("/agent/product_config")
.get()
.json();
const { data, error } = await getProductConfig();
if (data.value && !error.value) {
if (data.value.code === 200) {
productConfig.value = data.value.data.AgentProductConfig;
// 新系统数据结构data.value.data.list 是数组
productConfig.value = data.value.data.list || [];
// 根据接口返回的产品列表,生成报告类型配置
const types = [];
productConfig.value.forEach(config => {
if (config.product_en) {
types.push({
text: config.product_name,
value: config.product_en,
id: config.product_id,
});
}
});
reportTypes.value = types;
// 根据当前 feature 找到对应的报告类型,然后设置配置
// 如果没有 feature 参数,默认选择第一个报告类型
@@ -272,8 +288,8 @@ const getPromoteConfig = async () => {
}
// 如果没有找到匹配的报告类型或没有feature参数使用第一个报告类型
if (!reportType && reportTypes.length > 0) {
reportType = reportTypes[0];
if (!reportType && reportTypes.value.length > 0) {
reportType = reportTypes.value[0];
}
if (reportType) {
@@ -286,7 +302,7 @@ const getPromoteConfig = async () => {
};
const generatePromotionCode = async () => {
if (!pickerFieldVal.value) {
if (!pickerFieldVal.value || !pickerProductConfig.value) {
showToast({ message: '请选择报告类型' });
return;
}
@@ -298,23 +314,46 @@ const generatePromotionCode = async () => {
return;
}
// 保持原来的接口调用方式price 参数需要转换为 string 类型
// 使用 toFixed(2) 确保精度,避免浮点数精度问题
const priceStr = priceNum.toFixed(2);
// 验证价格范围
const minPrice = Number(pickerProductConfig.value.price_range_min) || 0;
const maxPrice = Number(pickerProductConfig.value.price_range_max) || Infinity;
const { data, error } = await useApiFetch("/agent/generating_link")
.post({ product: pickerFieldVal.value, price: priceStr })
.json();
if (priceNum < minPrice) {
showToast({ message: `价格不能低于 ${minPrice.toFixed(2)}` });
return;
}
if (data.value && !error.value) {
if (data.value.code === 200) {
linkIdentifier.value = data.value.data.link_identifier;
showQRcode.value = true;
if (priceNum > maxPrice) {
showToast({ message: `价格不能高于 ${maxPrice.toFixed(2)}` });
return;
}
try {
// 构建目标路径模板:推广报告页面路径(后端会将 linkIdentifier 拼接到路径中)
// 注意:后端会在重定向时自动将 linkIdentifier 拼接到 target_path 后面
const targetPath = `/agent/promotionInquire/`;
// 新系统API使用 product_id、set_price 和 target_path
const { data, error } = await generateLink({
product_id: pickerProductConfig.value.product_id,
set_price: priceNum,
target_path: targetPath
});
if (data.value && !error.value) {
if (data.value.code === 200) {
// 使用后端返回的完整短链
fullLink.value = data.value.data.full_link || "";
showQRcode.value = true;
} else {
console.log("Error generating promotion link", data.value);
showToast({ message: data.value.msg || '生成推广链接失败,请重试' });
}
} else {
console.log("Error generating promotion link", data.value);
showToast({ message: '生成推广链接失败,请重试' });
}
} else {
} catch (err) {
console.error('生成推广链接失败:', err);
showToast({ message: '生成推广链接失败,请重试' });
}
};
@@ -562,4 +601,4 @@ onMounted(async () => {
opacity: 0.4 !important;
color: #bdbdbd !important;
}
</style>
</style>

View File

@@ -1,28 +1,34 @@
<script setup>
const router = useRouter();
import { storeToRefs } from "pinia";
import SectionTitle from "@/components/SectionTitle.vue";
const agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore);
const { isAgent, isDiamond } = storeToRefs(agentStore);
const userStore = useUserStore();
const { isLoggedIn } = storeToRefs(userStore);
import personalDataIcon from "@/assets/images/promote/personal_data_bg.png";
import companyIcon from "@/assets/images/promote/company_bg.png";
import preLoanBackgroundCheckIcon from "@/assets/images/promote/preloan_background_check_bg.png";
import marriageRiskIcon from "@/assets/images/promote/marriage_risk_bg.png";
import housekeepingRiskIcon from "@/assets/images/promote/housekeeping_risk_bg.png";
import backgroundcheckIcon from "@/assets/images/promote/backgroundcheck_bg.png";
import consumerFinanceReportIcon from "@/assets/images/promote/consumer_finance_report_bg.png";
import banner1 from "@/assets/images/promote/banner_1.png";
import banner2 from "@/assets/images/promote/banner_2.png";
// 导入新设计图的图片资源
import bannerImg from "@/assets/images/index/n/01.jpg";
import productRecommendImg from "@/assets/images/index/n/02.png";
import inviteSubordinateImg from "@/assets/images/index/n/03.png";
import myTeamImg from "@/assets/images/index/n/04.png";
import personalDataImg from "@/assets/images/index/n/05.png";
import companyDataImg from "@/assets/images/index/n/07.png";
import consumerFinanceImg from "@/assets/images/index/n/08.png";
import housekeepingImg from "@/assets/images/index/n/09.png";
import marriageImg from "@/assets/images/index/n/06.png";
import backgroundCheckImg from "@/assets/images/index/n/10.png";
import upgradeBannerImg from "@/assets/images/index/n/11.png";
function toInquire(name) {
// 跳转到推广页面,传递 feature 参数
router.push({ name: "agentPromote", query: { feature: name } });
}
function toInvitation() {
router.push({ name: "invitation" });
}
const toPromote = () => {
router.push({ name: "agentPromote" });
};
@@ -47,302 +53,285 @@ const toWithdraw = () => {
router.push({ name: "withdraw" });
};
const toSubordinateList = () => {
router.push({ name: "subordinateList" });
const toTeamList = () => {
router.push({ name: "teamList" });
};
// 个人大数据查询
const toPersonalDataQuery = () => {
router.push({ name: "agentPromote", query: { feature: "riskassessment" } });
};
const services = ref([
{
// 产品UI配置映射根据 product_en 匹配)
const productUIConfig = {
'riskassessment': {
title: "个人大数据",
name: "personalData",
subtitle: "数据洞察,规避风险",
bg: personalDataIcon,
subtitle: "个人信用 精准查询",
bg: personalDataImg,
goColor: "#6699ff",
productId: 27,
costPrice: null,
isHighlight: true, // 重点推荐
},
{
title: "婚恋风险",
name: "marriage",
subtitle: "深度了解,坦诚相爱",
bg: marriageRiskIcon,
'marriage': {
title: "情侣报告",
subtitle: "相信才能相依 相爱才能永久",
bg: marriageImg,
goColor: "#ff99cc",
productId: 4,
costPrice: null,
},
{
'backgroundcheck': {
title: "入职背调",
name: "backgroundcheck",
subtitle: "人才甄选,用人无忧",
bg: backgroundcheckIcon,
subtitle: "查询便可 慧眼识英雄",
bg: backgroundCheckImg,
goColor: "#7db3ff",
productId: 1,
costPrice: null,
},
{
title: "小微企业",
name: "companyinfo",
subtitle: "风险监控,稳健经营",
bg: companyIcon,
'companyinfo': {
title: "企业大数据",
subtitle: "信任是合作 永恒的基石",
bg: companyDataImg,
goColor: "#ffaa66",
productId: 2,
costPrice: null,
},
{
title: "家政风险",
name: "homeservice",
subtitle: "身份核验,守护家庭",
bg: housekeepingRiskIcon,
'homeservice': {
title: "家政报告",
subtitle: "口碑与能力 一查便知",
bg: housekeepingImg,
goColor: "#66cccc",
productId: 3,
costPrice: null,
},
{
'consumerFinanceReport': {
title: "消金报告",
name: "consumerFinanceReport",
subtitle: "深度分析,智能风控",
bg: consumerFinanceReportIcon,
goColor: "#ffaa66",
productId: 28, // 使用贷前风险的id因为可能是同一个产品
costPrice: null,
subtitle: "",
bg: consumerFinanceImg,
goColor: "#a259ff",
},
// {
// title: "贷前背调",
// name: "preloanbackgroundcheck",
// subtitle: "风险识别,放款无忧",
// bg: preLoanBackgroundCheckIcon,
// goColor: "#f697b6",
// productId: 5,
// costPrice: null,
// },
]);
// 使用合并后的服务数组
const allServices = computed(() => services.value);
// 获取成本价显示文本
const getCostPriceText = (service) => {
// 如果已登录且是代理,且有成本价,显示成本价
if (isLoggedIn.value && isAgent.value && service.costPrice) {
return `成本价 ${service.costPrice}`;
}
// 如果未登录,显示登录提示
if (!isLoggedIn.value) {
return '登录查看';
}
// 如果已登录但不是代理,显示成为代理提示
if (!isAgent.value) {
return '成为代理查看';
}
// 默认情况
return '成本价 --';
};
// 获取产品配置和成本价
const getProductConfig = async () => {
try {
const { data, error } = await useApiFetch("/agent/product_config")
.get()
.json();
if (data.value && !error.value && data.value.code === 200) {
const productConfigList = data.value.data.AgentProductConfig;
// 更新每个服务的成本价
services.value.forEach(service => {
const config = productConfigList.find(
item => item.product_id === service.productId
);
if (config && config.cost_price !== undefined) {
service.costPrice = parseFloat(config.cost_price).toFixed(2);
}
});
} else {
console.log("Error fetching product config", data.value);
}
} catch (error) {
console.error("Failed to fetch product config", error);
}
};
onMounted(() => {
// 只有在已登录且是代理的情况下才请求产品配置接口
if (isLoggedIn.value && isAgent.value) {
getProductConfig();
}
// 个人大数据服务(重点推荐,单独处理)
const personalDataService = ref({
...productUIConfig['riskassessment'],
name: "riskassessment",
});
const noticeText = ref([]);
const toCooperation = () => {
window.location.href = "https://www.tianyuandata.com";
};
const toBigData = () => {
window.location.href = "https://www.tybigdata.com/";
// 其他服务列表(排除个人大数据,默认显示所有产品)
const otherServices = computed(() => {
return Object.keys(productUIConfig)
.filter(key => key !== 'riskassessment')
.map(key => ({
...productUIConfig[key],
name: key,
}));
});
// 获取服务对象的计算属性
const companyService = computed(() => otherServices.value.find(s => s.name === 'companyinfo'));
const consumerFinanceService = computed(() => otherServices.value.find(s => s.name === 'consumerFinanceReport'));
const housekeepingService = computed(() => otherServices.value.find(s => s.name === 'homeservice'));
const marriageService = computed(() => otherServices.value.find(s => s.name === 'marriage'));
const backgroundCheckService = computed(() => otherServices.value.find(s => s.name === 'backgroundcheck'));
// 将subtitle按照空格分割成两行
const formatSubtitle = (subtitle) => {
if (!subtitle) return '';
// 按照空格分割,取前两部分
const parts = subtitle.split(/\s+/);
if (parts.length >= 2) {
// 找到中间位置,尽量平均分配
const mid = Math.ceil(parts.length / 2);
const firstLine = parts.slice(0, mid).join(' ');
const secondLine = parts.slice(mid).join(' ');
return `${firstLine}\n${secondLine}`;
}
return subtitle;
};
// 轮播图数据
const bannerImages = [banner1, banner2];
// 代理商升级计划点击事件
const toUpgrade = () => {
// 如果不是钻石代理,跳转到升级页面
if (!isDiamond.value) {
router.push("/agent/upgrade");
}
};
</script>
<template>
<div class="box-border from-blue-100 to-white bg-gradient-to-b">
<div class="relative">
<van-swipe :autoplay="3000" indicator-color="white">
<van-swipe-item v-for="(banner, index) in bannerImages" :key="index" @click="toPromote">
<img class="h-full w-full" :src="banner" />
</van-swipe-item>
</van-swipe>
<div class="box-border bg-white min-h-screen">
<!-- 顶部横幅 -->
<div class="relative w-full" @click="toPersonalDataQuery">
<img :src="bannerImg" class="w-full" alt="一站式大数据 风险评估平台" />
</div>
<div class="px-6 mt-4">
<!-- 菜单项两排8个 -->
<div class="grid grid-cols-4 gap-3">
<!-- 第一排 -->
<div class="text-center flex flex-col justify-center items-center" @click="toPromote">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/tgbg.png" alt="推广报告" class="h-14 w-14" />
</div>
<div class="text-center mt-1 font-bold text-sm">推广报告</div>
</div>
<div class="text-center flex flex-col justify-center items-center" @click="toInvitation">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/yqxj.png" alt="邀请下级" class="h-14 w-14" />
<!-- 服务类别网格 -->
<div class="px-4 py-4">
<!-- 第一排三个卡片 -->
<div class="grid grid-cols-3 gap-3 mb-3">
<div class="flex flex-col items-center justify-center" @click="toPromote">
<img :src="productRecommendImg" class="w-20 h-20 rounded-lg shadow-sm object-cover" alt="产品推荐" />
<div class="mt-2 text-center text-gray-700 text-sm font-bold">
产品推荐
</div>
<div class="text-center mt-1 font-bold text-sm">邀请下级</div>
</div>
<div class="text-center flex flex-col justify-center items-center" @click="toHelp">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/bzzx.png" alt="帮助中心" class="h-14 w-14" />
<div class="flex flex-col items-center justify-center" @click="toInvitation">
<img :src="inviteSubordinateImg" class="w-20 h-20 rounded-lg shadow-sm object-cover" alt="邀请下级" />
<div class="mt-2 text-center text-gray-700 text-sm font-bold">
邀请下级
</div>
<div class="text-center mt-1 font-bold text-sm">帮助中心</div>
</div>
<div class="text-center flex flex-col justify-center items-center" @click="toHistory">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/wdbg.png" alt="我的报告" class="h-14 w-14" />
<div class="flex flex-col items-center justify-center" @click="toTeamList">
<img :src="myTeamImg" class="w-20 h-20 rounded-lg shadow-sm object-cover" alt="我的团队" />
<div class="mt-2 text-center text-gray-700 text-sm font-bold">
我的团队
</div>
<div class="text-center mt-1 font-bold text-sm">我的报告</div>
</div>
<!-- 第二排 -->
<div class="text-center flex flex-col justify-center items-center" @click="toAgent">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/zc.png" alt="资产" class="h-14 w-14" />
</div>
<div class="text-center mt-1 font-bold text-sm">资产</div>
</div>
<div class="text-center flex flex-col justify-center items-center" @click="toWithdraw">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/wytx.png" alt="我要提现" class="h-14 w-14" />
</div>
<div class="text-center mt-1 font-bold text-sm">我要提现</div>
</div>
<div class="text-center flex flex-col justify-center items-center" @click="toSubordinateList">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/wdxj.png" alt="我的下级" class="h-14 w-14" />
</div>
<div class="text-center mt-1 font-bold text-sm">我的下级</div>
</div>
<div class="text-center flex flex-col justify-center items-center" @click="toService">
<div
class="h-14 w-14 bg-gradient-to-b from-white to-blue-100/10 rounded-full shadow-lg flex items-center justify-center">
<img src="@/assets/images/promote/zxkf.png" alt="在线客服" class="h-14 w-14" />
</div>
<div class="text-center mt-1 font-bold text-sm">在线客服</div>
</div>
</div>
</div>
<div class="my-2 mx-4 rounded-xl overflow-hiddenshadow-xl" @click="toInvitation">
<img src="@/assets/images/promote/tghb.png" class="w-full h-full" alt="推广横幅" mode="widthFix" />
</div>
<div class="flex items-center justify-between mx-4 mb-2">
<SectionTitle title="推广服务" />
<div class="text-xs text-gray-500 flex items-center gap-1.5">
<span class="opacity-80"></span>
<span>滑动查看更多</span>
<span class="opacity-80"></span>
</div>
</div>
<div class="relative p-4 pt-0">
<div class="services-scroll-container">
<div class="services-scroll-wrapper">
<template v-for="(service, index) in allServices" :key="index">
<div class="relative flex flex-col px-4 py-2 rounded-xl shadow service-card"
:style="`background: url(${service.bg}) no-repeat; background-size: contain; background-position: center;`"
@click="toInquire(service.name)">
<!-- 第二排和第三排左侧大卡片 + 右侧小卡片 -->
<div class="grid grid-cols-2 gap-3">
<!-- 左侧个人大数据和情侣报告 -->
<div class="flex flex-col gap-3">
<!-- 个人大数据重点推荐- 占2.5个高度 -->
<div class="relative flex-[2.5]" @click="toPersonalDataQuery">
<div class="relative h-full rounded-lg overflow-hidden shadow-md flex flex-col px-4 py-2"
:style="`background: url(${personalDataImg}) no-repeat; background-size: cover; background-position: center;`">
<div class="flex flex-col items-start flex-1">
<div class="mt-1 text-left text-gray-600 font-bold">
{{ service.title }}
<div class="mt-1 max-w-max text-left text-gray-600 font-bold text-lg">
{{ personalDataService.title }}
</div>
<div class="mt-2 rounded-lg px-2 py-1 text-xs text-white w-max flex items-center"
:style="`background-color: ${service.goColor}`">
立即推广
<div class="text-left text-sm text-gray-500 leading-relaxed whitespace-pre-line"
style="max-width: calc(100% - 1rem);">
{{ formatSubtitle(personalDataService.subtitle) }}
</div>
<div class="mt-2 rounded-lg px-1 text-sm text-white shadow-xl w-max flex items-center"
:style="`background-color: ${personalDataService.goColor}`">
GO
<img src="@/assets/images/index/go_icon.png" alt="右箭头"
class="ml-0.5 h-4 w-4 inline-block align-middle" />
</div>
</div>
</div>
</div>
<!-- 情侣报告 - 占1.5个高度 -->
<template v-if="marriageService">
<div class="relative flex-[1.5]" @click="toInquire('marriage')">
<div class="relative h-full rounded-lg overflow-hidden shadow-md flex flex-col px-3 py-2"
:style="`background: url(${marriageImg}) no-repeat; background-size: cover; background-position: center;`">
<div class="flex flex-col items-start flex-1">
<div class="mt-1 max-w-max text-left text-gray-600 font-bold text-lg">
{{ marriageService.title }}
</div>
<div class="text-left text-sm text-gray-500 leading-relaxed whitespace-pre-line"
style="max-width: calc(100% - 1rem);">
{{ formatSubtitle(marriageService.subtitle) }}
</div>
</div>
</div>
</div>
</template>
</div>
<!-- 右侧四个小卡片 -->
<div class="flex flex-col gap-3">
<!-- 企业大数据 -->
<template v-if="companyService">
<div class="relative rounded-lg overflow-hidden shadow-md h-auto flex flex-col px-3 py-2 min-h-28"
:style="`background: url(${companyDataImg}) no-repeat; background-size: cover; background-position: center;`"
@click="toInquire('companyinfo')">
<div class="flex flex-col items-start flex-1">
<div class="mt-1 max-w-max text-left text-gray-600 font-bold text-lg">
{{ companyService.title }}
</div>
<div class="text-left text-sm text-gray-500 leading-relaxed whitespace-pre-line"
style="max-width: calc(100% - 1rem);">
{{ formatSubtitle(companyService.subtitle) }}
</div>
</div>
</div>
</template>
<!-- 消金报告 -->
<template v-if="consumerFinanceService">
<div class="relative rounded-lg overflow-hidden shadow-md h-auto flex flex-col px-3 py-2 min-h-28"
:style="`background: url(${consumerFinanceImg}) no-repeat; background-size: cover; background-position: center;`"
@click="toInquire('consumerFinanceReport')">
<div class="flex flex-col items-start flex-1">
<div class="mt-1 max-w-max text-left text-gray-600 font-bold text-lg">
{{ consumerFinanceService.title }}
</div>
<div v-if="consumerFinanceService.subtitle"
class="text-left text-sm text-gray-500 leading-relaxed whitespace-pre-line"
style="max-width: calc(100% - 1rem);">
{{ formatSubtitle(consumerFinanceService.subtitle) }}
</div>
<div class="mt-1 rounded-lg px-1 text-xs text-white shadow-xl w-max flex items-center"
:style="`background-color: ${consumerFinanceService.goColor}`">
GO
<img src="@/assets/images/index/go_icon.png" alt="右箭头"
class="ml-0.5 h-3 w-3 inline-block align-middle" />
</div>
</div>
<div
class="absolute bottom-0 left-0 right-0 rounded-b-xl px-2 py-1 text-xs text-white text-center">
{{ getCostPriceText(service) }}
</div>
</template>
<!-- 家政报告 -->
<template v-if="housekeepingService">
<div class="relative rounded-lg overflow-hidden shadow-md h-auto flex flex-col px-3 py-2 min-h-28"
:style="`background: url(${housekeepingImg}) no-repeat; background-size: cover; background-position: center;`"
@click="toInquire('homeservice')">
<div class="flex flex-col items-start flex-1">
<div class="mt-1 max-w-max text-left text-gray-600 font-bold text-lg">
{{ housekeepingService.title }}
</div>
<div class="text-left text-sm text-gray-500 leading-relaxed whitespace-pre-line"
style="max-width: calc(100% - 1rem);">
{{ formatSubtitle(housekeepingService.subtitle) }}
</div>
</div>
</div>
</template>
<!-- 入职背调 -->
<template v-if="backgroundCheckService">
<div class="relative rounded-lg overflow-hidden shadow-md h-auto flex flex-col px-3 py-2 min-h-28"
:style="`background: url(${backgroundCheckImg}) no-repeat; background-size: cover; background-position: center;`"
@click="toInquire('backgroundcheck')">
<div class="flex flex-col items-start flex-1">
<div class="mt-1 max-w-max text-left text-gray-600 font-bold text-lg">
{{ backgroundCheckService.title }}
</div>
<div class="text-left text-sm text-gray-500 leading-relaxed whitespace-pre-line"
style="max-width: calc(100% - 1rem);">
{{ formatSubtitle(backgroundCheckService.subtitle) }}
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- 底部横幅代理商升级计划仅非钻石代理显示 -->
<div v-if="!isDiamond" class="px-4 pb-4" @click="toUpgrade">
<img :src="upgradeBannerImg" class="w-full rounded-lg shadow-md" alt="代理商升级计划启动" />
</div>
</div>
</template>
<style scoped>
.clip-left {
clip-path: polygon(0 0, 0 100%, 90% 100%, 0 100%);
/* 确保图片显示正常 */
img {
display: block;
max-width: 100%;
height: auto;
}
.clip-right {
clip-path: polygon(0 0, 0 0, 90% 100%, 0 0);
/* 卡片点击效果 */
div[class*="rounded-lg"] {
transition: transform 0.2s, box-shadow 0.2s;
}
.services-scroll-container {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
padding-bottom: 0.5rem;
}
.services-scroll-container::-webkit-scrollbar {
display: none;
/* Chrome, Safari, Opera */
}
.services-scroll-wrapper {
display: flex;
gap: 1rem;
padding-right: 1rem;
}
.service-card {
width: 120px;
height: 120px;
flex-shrink: 0;
div[class*="rounded-lg"]:active {
transform: scale(0.98);
opacity: 0.9;
}
</style>

View File

@@ -26,17 +26,21 @@ function isFinishPayment() {
const query = new URLSearchParams(window.location.search);
let orderNo = query.get("out_trade_no");
if (orderNo) {
router.push({ path: "/report", query: { orderNo } });
console.log("🎯 Detected payment result, navigating to report with orderNo:", orderNo);
// 延迟跳转,确保页面已完全加载
setTimeout(() => {
router.push({ path: "/report", query: { orderNo } });
}, 100);
}
}
import { getLinkData } from "@/api/agent";
async function getProduct() {
linkIdentifier.value = route.params.linkIdentifier;
const { data: agentLinkData, error: agentLinkError } = await useApiFetch(
`/agent/link?link_identifier=${linkIdentifier.value}`
)
.get()
.json();
const { data: agentLinkData, error: agentLinkError } = await getLinkData(
linkIdentifier.value
);
if (agentLinkData.value && !agentLinkError.value) {
if (agentLinkData.value.code === 200) {
feature.value = agentLinkData.value.data.product_en;

426
src/views/Register.vue Normal file
View File

@@ -0,0 +1,426 @@
<script setup>
import { ref, computed, onUnmounted, onMounted, nextTick } from 'vue'
import { showToast } from 'vant'
import { registerByInviteCode, applyForAgent } from '@/api/agent'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { useRoute, useRouter } from 'vue-router'
import useApiFetch from '@/composables/useApiFetch'
const router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const phoneNumber = ref('')
const verificationCode = ref('')
const inviteCode = ref('')
const isAgreed = ref(false)
const isCountingDown = ref(false)
const countdown = ref(60)
let timer = null
// 从URL参数中读取邀请码并自动填入如果用户已登录且有手机号则自动填充
onMounted(async () => {
const inviteCodeParam = route.query.invite_code;
if (inviteCodeParam) {
inviteCode.value = inviteCodeParam;
}
// 如果用户已登录且有手机号,自动填充手机号
const token = localStorage.getItem("token");
if (token) {
// 确保用户信息已加载
if (!userStore.mobile) {
await userStore.fetchUserInfo();
}
if (userStore.mobile) {
phoneNumber.value = userStore.mobile;
}
}
});
// 是否是已注册用户(根据注册接口返回判断)
const isRegisteredUser = ref(false)
const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value)
})
const isInviteCodeValid = computed(() => {
return inviteCode.value.trim().length > 0
})
const canRegister = computed(() => {
return isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isInviteCodeValid.value &&
isAgreed.value
})
async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return
}
if (!isInviteCodeValid.value) {
showToast({ message: "请先输入邀请码" });
return
}
const { data, error } = await useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'agentApply' })
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
showToast({ message: "获取成功" });
startCountdown()
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
});
} else {
showToast(data.value.msg)
}
}
}
function startCountdown() {
isCountingDown.value = true
countdown.value = 60
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
} else {
clearInterval(timer)
isCountingDown.value = false
}
}, 1000)
}
async function handleRegister() {
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return
}
if (!isInviteCodeValid.value) {
showToast({ message: "请输入邀请码" });
return
}
if (verificationCode.value.length !== 6) {
showToast({ message: "请输入有效的验证码" });
return
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" });
return
}
// 直接执行注册逻辑
performRegister()
}
// 执行实际的注册逻辑
async function performRegister() {
try {
// 先尝试通过邀请码注册(同时注册用户和代理)
const { data, error } = await registerByInviteCode({
mobile: phoneNumber.value,
code: verificationCode.value,
referrer: inviteCode.value.trim()
})
if (data.value && !error.value) {
if (data.value.code === 200) {
// 保存token
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
// 更新代理信息到store
if (data.value.data.agent_id) {
agentStore.updateAgentInfo({
isAgent: true,
agentID: data.value.data.agent_id,
level: data.value.data.level || 1,
levelName: data.value.data.level_name || '普通代理'
})
}
showToast({ message: "注册成功!" });
// 跳转到代理主页
setTimeout(() => {
window.location.href = '/'
}, 500)
}
}
} catch (err) {
console.error('注册失败:', err)
showToast({ message: "注册失败,请重试" });
}
}
// 已注册用户申请成为代理
async function applyForAgentAsRegisteredUser() {
try {
const { data, error } = await applyForAgent({
mobile: phoneNumber.value,
code: verificationCode.value,
referrer: inviteCode.value.trim()
})
if (data.value && !error.value) {
if (data.value.code === 200) {
// 保存token
localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire)
showToast({ message: "申请成功!" });
// 跳转到代理主页
setTimeout(() => {
window.location.href = '/'
}, 500)
} else {
showToast(data.value.msg || "申请失败,请重试")
}
}
} catch (err) {
console.error('申请失败:', err)
showToast({ message: "申请失败,请重试" });
}
}
function toUserAgreement() {
router.push(`/userAgreement`)
}
function toPrivacyPolicy() {
router.push(`/privacyPolicy`)
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
const onClickLeft = () => {
router.replace('/')
}
</script>
<template>
<div class="login-layout ">
<van-nav-bar fixed placeholder title="注册成为代理" left-text="" left-arrow @click-left="onClickLeft" />
<div class="login px-4 relative z-10">
<div class="mb-8 pt-20 text-left">
<div class="flex flex-col items-center">
<img class="h-16 w-16 rounded-full shadow" src="@/assets/images/logo.png" alt="Logo" />
<div class="text-3xl mt-4 text-slate-700 font-bold">一查查</div>
</div>
</div>
<!-- 注册表单 -->
<div class="login-form">
<!-- 邀请码输入 -->
<div class="form-item">
<div class="form-label">邀请码</div>
<input v-model="inviteCode" class="form-input" type="text" placeholder="请输入邀请码" />
</div>
<!-- 手机号输入 -->
<div class="form-item">
<div class="form-label">手机号</div>
<input v-model="phoneNumber" class="form-input" type="tel" placeholder="请输入手机号" maxlength="11" />
</div>
<!-- 验证码输入 -->
<div class="form-item">
<div class="form-label">验证码</div>
<div class="verification-input-wrapper">
<input v-model="verificationCode" id="verificationCode" class="form-input verification-input"
placeholder="请输入验证码" maxlength="6" />
<button class="get-code-btn"
:class="{ 'disabled': isCountingDown || !isPhoneNumberValid || !isInviteCodeValid }"
@click="sendVerificationCode"
:disabled="isCountingDown || !isPhoneNumberValid || !isInviteCodeValid">
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
</button>
</div>
</div>
<!-- 协议同意框 -->
<div class="agreement-wrapper">
<input type="checkbox" v-model="isAgreed" class="agreement-checkbox accent-primary"
id="agreement" />
<label for="agreement" class="agreement-text">
我已阅读并同意
<a class="agreement-link" @click="toUserAgreement">用户协议</a>
<a class="agreement-link" @click="toPrivacyPolicy">隐私政策</a>
</label>
</div>
<!-- 提示文字 -->
<div class="notice-text">
未注册手机号注册后将自动生成账号并成为代理并且代表您已阅读并同意
</div>
<!-- 注册按钮 -->
<button class="login-btn" :class="{ 'disabled': !canRegister }" @click="handleRegister"
:disabled="!canRegister">
注册成为代理
</button>
</div>
</div>
</div>
</template>
<style scoped>
.login-layout {
background-image: url('@/assets/images/login_bg.png');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
height: 100vh;
position: relative;
overflow: hidden;
}
/* 登录表单 */
.login-form {
background-color: var(--color-bg-primary);
padding: 2rem;
margin-top: 0.5rem;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
border-radius: 8px;
}
/* 表单项 */
.form-item {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
border: none;
border-bottom: 1px solid var(--color-border-primary);
}
.form-label {
font-size: 0.9375rem;
color: var(--color-text-primary);
margin-bottom: 0;
margin-right: 1rem;
font-weight: 500;
min-width: 4rem;
flex-shrink: 0;
}
.form-input {
width: 100%;
padding: 0.875rem 0;
font-size: 0.9375rem;
color: var(--color-text-primary);
outline: none;
background-color: transparent;
}
.form-input::placeholder {
color: var(--color-text-tertiary);
}
.form-input:focus {
border-bottom-color: var(--color-text-primary);
}
/* 验证码输入 */
.verification-input-wrapper {
position: relative;
display: flex;
align-items: center;
flex: 1;
}
.verification-input {
flex: 1;
padding-right: 6rem;
}
.get-code-btn {
position: absolute;
right: 0;
background: none;
border: none;
color: var(--color-primary);
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
font-weight: 500;
}
.get-code-btn.disabled {
color: var(--color-gray-400);
cursor: not-allowed;
}
/* 协议同意 */
.agreement-wrapper {
display: flex;
align-items: center;
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.agreement-checkbox {
flex-shrink: 0;
margin-right: 0.5rem;
}
.agreement-text {
font-size: 0.75rem;
color: var(--color-text-secondary);
line-height: 1.4;
}
.agreement-link {
color: var(--color-primary);
cursor: pointer;
text-decoration: none;
}
/* 提示文字 */
.notice-text {
font-size: 0.6875rem;
color: var(--color-text-tertiary);
line-height: 1.5;
margin-bottom: 2rem;
}
/* 登录按钮 */
.login-btn {
width: 100%;
padding: 0.875rem;
background-color: var(--color-primary);
color: var(--color-text-white);
border: none;
border-radius: 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: opacity 0.3s;
letter-spacing: 0.25rem;
}
.login-btn:hover {
opacity: 0.9;
}
.login-btn.disabled {
background-color: var(--color-gray-300);
cursor: not-allowed;
}
</style>

View File

@@ -10,7 +10,7 @@
onMounted(() => {
(function (d, t) {
const BASE_URL = "https://service.quannengcha.com";
const BASE_URL = "https://service.onecha.cn";
const g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src = BASE_URL + "/packs/js/sdk.js";

View File

@@ -9,14 +9,27 @@ const refreshing = ref(false)
const finished = ref(false)
const page = ref(1)
const pageSize = 8
const activeTab = ref('order') // 'order' 或 'invite'
// 获取收益列表
const fetchRewardDetails = async () => {
if (loading.value || finished.value) return
// 数据
const userInfo = ref({})
const orderStats = ref({})
const rebateStats = ref({})
const inviteStats = ref({})
const orderList = ref([])
const inviteList = ref([])
const orderListTotal = ref(0)
const inviteListTotal = ref(0)
// 获取详情数据
const fetchDetail = async () => {
if (loading.value) return
if (finished.value && page.value > 1) return
loading.value = true
const tabType = activeTab.value
const { data, error } = await useApiFetch(
`/agent/subordinate/contribution/detail?subordinate_id=${route.params.id}&page=${page.value}&page_size=${pageSize}`
`/agent/subordinate/contribution/detail?subordinate_id=${route.params.id}&page=${page.value}&page_size=${pageSize}&tab_type=${tabType}`
)
.get()
.json()
@@ -30,201 +43,107 @@ const fetchRewardDetails = async () => {
level: data.value.data.level_name || '普通',
mobile: data.value.data.mobile,
}
// 更新汇总数据
summary.value = {
totalReward: data.value.data.total_earnings,
totalContribution: data.value.data.total_contribution,
totalOrders: data.value.data.total_orders,
}
// 设置默认的统计类型
statistics.value = [
{
type: 'descendant_promotion',
amount: 0,
count: 0,
description: '推广奖励',
},
{
type: 'cost',
amount: 0,
count: 0,
description: '成本贡献',
},
{
type: 'pricing',
amount: 0,
count: 0,
description: '定价贡献',
},
{
type: 'descendant_withdraw',
amount: 0,
count: 0,
description: '提现收益',
},
{
type: 'descendant_upgrade_vip',
amount: 0,
count: 0,
description: '转化VIP奖励',
},
{
type: 'descendant_upgrade_svip',
amount: 0,
count: 0,
description: '转化SVIP奖励',
},
{
type: 'descendant_new_active',
amount: 0,
count: 0,
description: '新增活跃奖励',
},
{
type: 'descendant_stay_active',
amount: 0,
count: 0,
description: '月度活跃奖励',
},
]
// 如果有统计数据,更新对应的值
if (data.value.data.stats) {
const stats = data.value.data.stats
// 更新推广奖励
const platformStat = statistics.value.find(s => s.type === 'descendant_promotion')
if (platformStat) {
platformStat.amount = stats.descendant_promotion_amount || 0
platformStat.count = stats.descendant_promotion_count || 0
}
// 更新成本贡献
const costStat = statistics.value.find(s => s.type === 'cost')
if (costStat) {
costStat.amount = stats.cost_amount || 0
costStat.count = stats.cost_count || 0
}
// 更新定价贡献
const pricingStat = statistics.value.find(s => s.type === 'pricing')
if (pricingStat) {
pricingStat.amount = stats.pricing_amount || 0
pricingStat.count = stats.pricing_count || 0
}
// 更新提现收益
const withdrawStat = statistics.value.find(s => s.type === 'descendant_withdraw')
if (withdrawStat) {
withdrawStat.amount = stats.descendant_withdraw_amount || 0
withdrawStat.count = stats.descendant_withdraw_count || 0
}
// 更新转化VIP奖励
const conversionVipStat = statistics.value.find(s => s.type === 'descendant_upgrade_vip')
if (conversionVipStat) {
conversionVipStat.amount = stats.descendant_upgrade_vip_amount || 0
conversionVipStat.count = stats.descendant_upgrade_vip_count || 0
}
// 更新转化SVIP奖励
const conversionSvipStat = statistics.value.find(s => s.type === 'descendant_upgrade_svip')
if (conversionSvipStat) {
conversionSvipStat.amount = stats.descendant_upgrade_svip_amount || 0
conversionSvipStat.count = stats.descendant_upgrade_svip_count || 0
}
// 更新活跃奖励
const activeStat = statistics.value.find(s => s.type === 'descendant_new_active')
if (activeStat) {
activeStat.amount = stats.descendant_new_active_amount || 0
activeStat.count = stats.descendant_new_active_count || 0
}
// 更新月度活跃奖励
const monthlyActiveStat = statistics.value.find(s => s.type === 'descendant_stay_active')
if (monthlyActiveStat) {
monthlyActiveStat.amount = stats.descendant_stay_active_amount || 0
monthlyActiveStat.count = stats.descendant_stay_active_count || 0
}
}
rewardDetails.value = []
}
// 处理列表数据
if (data.value.data.list) {
if (page.value === 1) {
rewardDetails.value = data.value.data.list
// 更新统计数据
orderStats.value = data.value.data.order_stats || {}
rebateStats.value = data.value.data.rebate_stats || {}
inviteStats.value = data.value.data.invite_stats || {}
// 清空列表
if (tabType === 'order') {
orderList.value = []
} else {
rewardDetails.value.push(...data.value.data.list)
inviteList.value = []
}
}
// 处理列表数据
if (tabType === 'order') {
if (data.value.data.order_list) {
if (page.value === 1) {
orderList.value = data.value.data.order_list
} else {
orderList.value.push(...data.value.data.order_list)
}
orderListTotal.value = data.value.data.order_list_total || 0
finished.value = data.value.data.order_list.length < pageSize
if (!finished.value) {
page.value++
}
} else {
finished.value = true
}
finished.value = data.value.data.list.length < pageSize
} else {
finished.value = true
if (data.value.data.invite_list) {
if (page.value === 1) {
inviteList.value = data.value.data.invite_list
} else {
inviteList.value.push(...data.value.data.invite_list)
}
inviteListTotal.value = data.value.data.invite_list_total || 0
finished.value = data.value.data.invite_list.length < pageSize
if (!finished.value) {
page.value++
}
} else {
finished.value = true
}
}
}
}
loading.value = false
}
// 切换标签页
const switchTab = (tab) => {
if (activeTab.value === tab) return
activeTab.value = tab
page.value = 1
finished.value = false
loading.value = false
// 延迟一下确保tab切换完成
setTimeout(() => {
fetchDetail()
}, 100)
}
// 下拉刷新
const onRefresh = () => {
finished.value = false
page.value = 1
fetchRewardDetails().finally(() => {
fetchDetail().finally(() => {
refreshing.value = false
})
}
const rewardDetails = ref([])
const userInfo = ref({})
const summary = ref({})
const statistics = ref([])
onMounted(() => {
fetchRewardDetails()
})
// 获取收益类型样式
const getRewardTypeClass = type => {
const typeMap = {
descendant_promotion: 'bg-blue-100 text-blue-600',
cost: 'bg-green-100 text-green-600',
pricing: 'bg-purple-100 text-purple-600',
descendant_withdraw: 'bg-yellow-100 text-yellow-600',
descendant_upgrade_vip: 'bg-red-100 text-red-600',
descendant_upgrade_svip: 'bg-orange-100 text-orange-600',
descendant_new_active: 'bg-indigo-100 text-indigo-600',
descendant_stay_active: 'bg-pink-100 text-pink-600',
// 获取等级标签样式
const getLevelClass = (level) => {
const levelNum = typeof level === 'number' ? level : parseInt(level)
switch (levelNum) {
case 3:
return 'bg-purple-100 text-purple-600'
case 2:
return 'bg-yellow-100 text-yellow-600'
case 1:
default:
return 'bg-gray-100 text-gray-600'
}
return typeMap[type] || 'bg-gray-100 text-gray-600'
}
// 获取收益类型图标
const getRewardTypeIcon = type => {
const iconMap = {
descendant_promotion: 'gift',
cost: 'gold-coin',
pricing: 'balance-pay',
descendant_withdraw: 'cash-back-record',
descendant_upgrade_vip: 'fire',
descendant_upgrade_svip: 'fire',
descendant_new_active: 'medal',
descendant_stay_active: 'medal',
// 获取等级显示名称
const getLevelName = (level) => {
const levelNum = typeof level === 'number' ? level : parseInt(level)
const levelMap = {
1: '普通',
2: '黄金',
3: '钻石'
}
return iconMap[type] || 'balance-o'
}
// 获取收益类型描述
const getRewardTypeDescription = type => {
const descriptionMap = {
descendant_promotion: '推广奖励',
cost: '成本贡献',
pricing: '定价贡献',
descendant_withdraw: '提现收益',
descendant_upgrade_vip: '转化VIP奖励',
descendant_upgrade_svip: '转化SVIP奖励',
descendant_new_active: '新增活跃奖励',
descendant_stay_active: '月度活跃奖励',
}
return descriptionMap[type] || '未知类型'
return levelMap[levelNum] || '普通'
}
// 格式化时间
const formatTime = timeStr => {
if (!timeStr) return '-'
const date = new Date(timeStr)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
return timeStr.split(' ')[0]
}
// 格式化金额
@@ -232,10 +151,14 @@ const formatNumber = num => {
if (!num) return '0.00'
return Number(num).toFixed(2)
}
onMounted(() => {
fetchDetail()
})
</script>
<template>
<div class="reward-detail">
<div class="subordinate-detail">
<!-- 用户信息卡片 -->
<div class="p-4">
<div class="bg-white rounded-xl shadow-sm p-5 mb-4">
@@ -248,104 +171,136 @@ const formatNumber = num => {
</div>
</div>
<div class="text-sm text-gray-500 mb-4">成为下级代理时间{{ formatTime(userInfo.createTime) }}</div>
<div class="grid grid-cols-3 gap-4">
</div>
<!-- 订单统计卡片 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<div class="text-base font-medium text-gray-800 mb-3">订单统计仅统计有返佣的订单</div>
<div class="grid grid-cols-3 gap-3">
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">推广单量</div>
<div class="text-xl font-semibold text-blue-600">{{ summary.totalOrders }}</div>
<div class="text-gray-500 text-sm mb-1">单量</div>
<div class="text-xl font-semibold text-blue-600">{{ orderStats.total_orders || 0 }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">总收益</div>
<div class="text-xl font-semibold text-green-600">¥{{ formatNumber(summary.totalReward) }}</div>
<div class="text-gray-500 text-sm mb-1">月订单</div>
<div class="text-xl font-semibold text-green-600">{{ orderStats.month_orders || 0 }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">总贡献</div>
<div class="text-xl font-semibold text-purple-600">¥{{ formatNumber(summary.totalContribution) }}</div>
<div class="text-gray-500 text-sm mb-1">今日订单</div>
<div class="text-xl font-semibold text-orange-600">{{ orderStats.today_orders || 0 }}</div>
</div>
</div>
</div>
<!-- 贡献统计卡片 -->
<!-- 返佣统计卡片 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<div class="text-base font-medium text-gray-800 mb-3">贡献统计</div>
<div class="grid grid-cols-2 gap-3">
<div
v-for="item in statistics"
:key="item.type"
class="flex items-center p-2 rounded-lg"
:class="getRewardTypeClass(item.type).split(' ')[0]"
>
<van-icon
:name="getRewardTypeIcon(item.type)"
class="text-lg mr-2"
:class="getRewardTypeClass(item.type).split(' ')[1]"
/>
<div class="flex-1">
<div class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
{{ item.description }}
</div>
<div class="flex justify-between items-center mt-1">
<div class="text-xs text-gray-500">{{ item.count }} </div>
<div class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
¥{{ formatNumber(item.amount) }}
</div>
</div>
<div class="text-base font-medium text-gray-800 mb-3">返佣统计</div>
<div class="grid grid-cols-3 gap-3">
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">总返佣金额</div>
<div class="text-xl font-semibold text-blue-600">¥{{ formatNumber(rebateStats.total_rebate_amount) }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">月返佣金额</div>
<div class="text-xl font-semibold text-green-600">¥{{ formatNumber(rebateStats.month_rebate_amount) }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">今日返佣金额</div>
<div class="text-xl font-semibold text-orange-600">¥{{ formatNumber(rebateStats.today_rebate_amount) }}
</div>
</div>
</div>
</div>
<!-- 邀请统计卡片 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<!-- 贡献记录列表 -->
<div class="text-base font-medium text-gray-800">贡献记录</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="fetchRewardDetails"
>
<div class="p-4">
<div v-if="rewardDetails.length === 0" class="text-center text-gray-500 py-8">暂无贡献记录</div>
<div v-else v-for="item in rewardDetails" :key="item.id" class="reward-item">
<div class="mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<van-icon
:name="getRewardTypeIcon(item.type)"
class="text-lg"
:class="getRewardTypeClass(item.type).split(' ')[1]"
/>
<div>
<div class="font-medium text-gray-800">{{ getRewardTypeDescription(item.type) }}</div>
<div class="text-base font-medium text-gray-800 mb-3">邀请统计</div>
<div class="grid grid-cols-3 gap-3">
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">总邀请</div>
<div class="text-xl font-semibold text-blue-600">{{ inviteStats.total_invites || 0 }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">月邀请</div>
<div class="text-xl font-semibold text-green-600">{{ inviteStats.month_invites || 0 }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">今日邀请</div>
<div class="text-xl font-semibold text-orange-600">{{ inviteStats.today_invites || 0 }}</div>
</div>
</div>
</div>
<!-- Tab标签页 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<van-tabs v-model:active="activeTab" @change="switchTab">
<van-tab title="订单列表" name="order">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchDetail">
<div class="p-2">
<div v-if="orderList.length === 0" class="text-center text-gray-500 py-8">暂无订单记录</div>
<div v-else v-for="item in orderList" :key="item.order_no"
class="order-item mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="font-medium text-gray-800 mb-1">{{ item.product_name || '未知产品' }}</div>
<div class="text-xs text-gray-500 mb-1">订单号{{ item.order_no }}</div>
<div class="text-xs text-gray-500">{{ formatTime(item.create_time) }}</div>
</div>
</div>
<div class="text-right">
<div class="text-base font-semibold" :class="getRewardTypeClass(item.type).split(' ')[1]">
¥{{ formatNumber(item.amount) }}
<div class="text-right ml-4">
<div class="text-sm text-gray-500 mb-1">订单金额</div>
<div class="text-base font-semibold text-blue-600 mb-2">¥{{ formatNumber(item.order_amount) }}
</div>
<div class="text-sm text-gray-500 mb-1">返佣金额</div>
<div class="text-base font-semibold text-green-600">¥{{ formatNumber(item.rebate_amount) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</van-list>
</van-pull-refresh>
</van-tab>
<van-tab title="邀请列表" name="invite">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchDetail">
<div class="p-2">
<div v-if="inviteList.length === 0" class="text-center text-gray-500 py-8">暂无邀请记录</div>
<div v-else v-for="item in inviteList" :key="item.agent_id"
class="invite-item mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3 flex-1">
<div class="text-lg font-semibold text-gray-800">{{ item.mobile }}</div>
<span :class="['px-3 py-1 rounded-full text-sm font-medium', getLevelClass(item.level)]">
{{ item.level_name }}代理
</span>
</div>
<div class="text-xs text-gray-500">{{ formatTime(item.create_time) }}</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</van-tab>
</van-tabs>
</div>
</div>
</div>
</template>
<style scoped>
.reward-detail {
.subordinate-detail {
min-height: 100vh;
background-color: #f5f5f5;
}
.reward-item {
.order-item,
.invite-item {
transition: transform 0.2s;
}
.reward-item:active {
.order-item:active,
.invite-item:active {
transform: scale(0.98);
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useAgentStore } from '@/stores/agentStore'
import useApiFetch from '@/composables/useApiFetch'
import { getSubordinateList } from '@/api/agent'
import { useRouter } from 'vue-router'
const agentStore = useAgentStore()
@@ -13,8 +13,17 @@ const pageSize = 8
const refreshing = ref(false)
const router = useRouter()
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await fetchSubordinates()
}
}
onBeforeMount(() => {
fetchSubordinates()
// 初始化时重置状态
page.value = 1
finished.value = false
})
// 计算统计数据
const statistics = ref({
@@ -26,21 +35,46 @@ const fetchSubordinates = async () => {
if (loading.value || finished.value) return
loading.value = true
const { data, error } = await useApiFetch(`/agent/subordinate/list?page=${page.value}&page_size=${pageSize}`)
.get()
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
statistics.value.totalSubordinates = data.value.data.total
if (page.value === 1) {
subordinates.value = data.value.data.list
try {
const { data, error } = await getSubordinateList({
page: page.value,
page_size: pageSize
})
if (data.value && !error.value) {
if (data.value.code === 200) {
statistics.value.totalSubordinates = data.value.data.total
if (page.value === 1) {
subordinates.value = data.value.data.list || []
} else {
subordinates.value.push(...(data.value.data.list || []))
}
// 判断是否加载完成
if (data.value.data.list && data.value.data.list.length < pageSize) {
finished.value = true
} else if (subordinates.value.length >= data.value.data.total) {
finished.value = true
} else {
page.value++
}
} else {
subordinates.value.push(...data.value.data.list)
// 接口返回错误,停止翻页
finished.value = true
console.error('获取下级列表失败:', data.value.msg || '未知错误')
}
} else {
// 请求失败或返回错误,停止翻页
finished.value = true
console.error('获取下级列表失败:', error.value || '请求失败')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取下级列表失败:', err)
} finally {
loading.value = false
}
finished.value = true
loading.value = false
}
// 下拉刷新
@@ -64,23 +98,37 @@ const formatNumber = num => {
return Number(num).toFixed(2)
}
// 获取等级标签样式
const getLevelClass = level => {
switch (level) {
case 'SVIP':
// 获取等级标签样式新系统1=普通2=黄金3=钻石)
const getLevelClass = (level) => {
// level可能是数字或字符串
const levelNum = typeof level === 'number' ? level : parseInt(level)
switch (levelNum) {
case 3:
return 'bg-purple-100 text-purple-600'
case 'VIP':
return 'bg-blue-100 text-blue-600'
case 2:
return 'bg-yellow-100 text-yellow-600'
case 1:
default:
return 'bg-gray-100 text-gray-600'
}
}
// 获取等级显示名称
const getLevelName = (level) => {
const levelNum = typeof level === 'number' ? level : parseInt(level)
const levelMap = {
1: '普通',
2: '黄金',
3: '钻石'
}
return levelMap[levelNum] || '普通'
}
// 查看详情
const viewDetail = item => {
router.push({
name: 'subordinateDetail',
params: { id: item.id },
params: { id: item.agent_id || item.id },
})
}
@@ -104,7 +152,7 @@ onMounted(() => {
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchSubordinates">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div class="p-4">
<div v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item">
<div class="flex flex-col p-5 bg-white rounded-xl shadow-sm mb-4">
@@ -112,13 +160,13 @@ onMounted(() => {
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div
class="w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-600 rounded-full text-sm font-medium"
>
class="w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-600 rounded-full text-sm font-medium">
{{ index + 1 }}
</div>
<div class="text-xl font-semibold text-gray-800">{{ item.mobile }}</div>
<span :class="['px-3 py-1 rounded-full text-sm font-medium', getLevelClass(item.level)]">
{{ item.level ? item.level : '普通' }}代理
<span
:class="['px-3 py-1 rounded-full text-sm font-medium', getLevelClass(item.level || item.level_name)]">
{{ getLevelName(item.level || item.level_name) }}代理
</span>
</div>
</div>
@@ -127,27 +175,21 @@ onMounted(() => {
<div class="text-sm text-gray-500 mb-5">成为下级代理时间{{ item.create_time }}</div>
<!-- 数据统计 -->
<div class="grid grid-cols-3 gap-6 mb-5">
<div class="grid grid-cols-2 gap-6 mb-5">
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">推广单量</div>
<div class="text-xl font-semibold text-blue-600">{{ item.total_orders }}</div>
<div class="text-gray-500 text-sm mb-1">订单数</div>
<div class="text-xl font-semibold text-blue-600">{{ item.total_orders || 0 }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">收益</div>
<div class="text-xl font-semibold text-green-600">¥{{ formatNumber(item.total_earnings) }}</div>
</div>
<div class="text-center">
<div class="text-gray-500 text-sm mb-1">总贡献</div>
<div class="text-xl font-semibold text-purple-600">¥{{ formatNumber(item.total_contribution) }}</div>
<div class="text-gray-500 text-sm mb-1">金额</div>
<div class="text-xl font-semibold text-green-600">¥{{ formatNumber(item.total_amount) }}</div>
</div>
</div>
<!-- 查看详情按钮 -->
<div class="flex justify-end">
<button
@click="viewDetail(item)"
class="inline-flex items-center px-4 py-2 text-sm bg-gradient-to-r from-blue-500 to-blue-400 text-white rounded-full shadow-sm hover:shadow-md transition-all duration-200"
>
<button @click="viewDetail(item)"
class="inline-flex items-center px-4 py-2 text-sm bg-gradient-to-r from-blue-500 to-blue-400 text-white rounded-full shadow-sm hover:shadow-md transition-all duration-200">
<van-icon name="eye" class="mr-1.5" />
查看详情
</button>

401
src/views/TeamList.vue Normal file
View File

@@ -0,0 +1,401 @@
<script setup>
import { ref, onMounted, onBeforeMount } from 'vue'
import { useAgentStore } from '@/stores/agentStore'
import { getTeamList } from '@/api/agent'
import { useRouter } from 'vue-router'
const agentStore = useAgentStore()
const teamMembers = ref([])
const loading = ref(false)
const finished = ref(false)
const page = ref(1)
const pageSize = 8
const refreshing = ref(false)
const router = useRouter()
const searchMobile = ref('')
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await fetchTeamMembers()
}
}
onBeforeMount(() => {
// 初始化时重置状态
page.value = 1
finished.value = false
})
// 获取团队列表
const fetchTeamMembers = async () => {
if (loading.value || finished.value) return
loading.value = true
try {
const params = {
page: page.value,
page_size: pageSize
}
// 如果有搜索条件,添加手机号参数
if (searchMobile.value && searchMobile.value.trim()) {
params.mobile = searchMobile.value.trim()
}
const { data, error } = await getTeamList(params)
if (data.value && !error.value) {
if (data.value.code === 200) {
if (page.value === 1) {
teamMembers.value = data.value.data.list || []
} else {
teamMembers.value.push(...(data.value.data.list || []))
}
// 判断是否加载完成
if (data.value.data.list && data.value.data.list.length < pageSize) {
finished.value = true
} else if (teamMembers.value.length >= data.value.data.total) {
finished.value = true
} else {
page.value++
}
} else {
// 接口返回错误,停止翻页
finished.value = true
console.error('获取团队列表失败:', data.value.msg || '未知错误')
}
} else {
// 请求失败或返回错误,停止翻页
finished.value = true
console.error('获取团队列表失败:', error.value || '请求失败')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取团队列表失败:', err)
} finally {
loading.value = false
}
}
// 下拉刷新
const onRefresh = () => {
finished.value = false
page.value = 1
fetchTeamMembers().finally(() => {
refreshing.value = false
})
}
// 搜索功能
const handleSearch = () => {
finished.value = false
page.value = 1
fetchTeamMembers()
}
// 清空搜索
const handleClear = () => {
searchMobile.value = ''
finished.value = false
page.value = 1
fetchTeamMembers()
}
// 格式化时间
const formatTime = timeStr => {
if (!timeStr) return '-'
return timeStr.split(' ')[0]
}
// 格式化金额
const formatNumber = num => {
if (!num) return '0.00'
return Number(num).toFixed(2)
}
// 格式化数字
const formatCount = num => {
if (!num) return '0'
return Number(num).toLocaleString()
}
// 获取等级标签样式新系统1=普通2=黄金3=钻石)
const getLevelClass = (level) => {
// level可能是数字或字符串
const levelNum = typeof level === 'number' ? level : parseInt(level)
switch (levelNum) {
case 3:
return 'bg-gray-100 text-gray-700'
case 2:
return 'bg-gray-100 text-gray-700'
case 1:
default:
return 'bg-gray-100 text-gray-700'
}
}
// 获取等级显示名称
const getLevelName = (level) => {
const levelNum = typeof level === 'number' ? level : parseInt(level)
const levelMap = {
1: '普通',
2: '黄金',
3: '钻石'
}
return levelMap[levelNum] || '普通'
}
// 查看详情
const viewDetail = item => {
router.push({
name: 'subordinateDetail',
params: { id: item.agent_id || item.id },
})
}
onMounted(() => {
fetchTeamMembers()
})
</script>
<template>
<div class="team-list">
<!-- 搜索框 -->
<div class="px-4 mt-4">
<div class="search-box">
<van-field v-model="searchMobile" placeholder="请输入手机号搜索" clearable @clear="handleClear"
@keyup.enter="handleSearch">
<template #button>
<van-button size="small" type="primary" @click="handleSearch" class="search-btn">搜索</van-button>
</template>
</van-field>
</div>
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div class="p-4">
<div v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="team-member-item">
<div class="member-card">
<!-- 顶部信息 -->
<div class="member-header">
<div class="member-index">{{ index + 1 }}</div>
<div class="member-mobile">{{ item.mobile }}</div>
<span :class="['member-level', getLevelClass(item.level || item.level_name)]">
{{ getLevelName(item.level || item.level_name) }}代理
</span>
<span :class="['member-relation', item.is_direct ? 'member-direct' : 'member-indirect']">
{{ item.is_direct ? '直接' : '间接' }}
</span>
</div>
<!-- 加入时间 -->
<div class="member-time">加入团队时间{{ item.create_time }}</div>
<!-- 数据统计 -->
<div class="member-stats">
<div class="member-stat-item">
<div class="member-stat-label">返佣总金额</div>
<div class="member-stat-value">¥{{ formatNumber(item.total_rebate_amount || 0) }}</div>
</div>
<div class="member-stat-item">
<div class="member-stat-label">今日推广</div>
<div class="member-stat-value">{{ formatCount(item.today_queries || 0) }}</div>
</div>
<div class="member-stat-item">
<div class="member-stat-label">邀请总人数</div>
<div class="member-stat-value">{{ formatCount(item.total_invites || 0) }}</div>
</div>
<div class="member-stat-item">
<div class="member-stat-label">总推广量 </div>
<div class="member-stat-value">{{ formatCount(item.total_queries || 0) }}</div>
</div>
<div class="member-stat-item">
<div class="member-stat-label">今日返佣</div>
<div class="member-stat-value">¥{{ formatNumber(item.today_rebate_amount || 0) }}</div>
</div>
<div class="member-stat-item">
<div class="member-stat-label">邀请今日</div>
<div class="member-stat-value">{{ formatCount(item.today_invites || 0) }}</div>
</div>
</div>
<!-- 查看详情按钮 -->
<div class="member-action">
<button @click="viewDetail(item)" class="detail-btn">
<van-icon name="eye" class="mr-1" />
查看详情
</button>
</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<style scoped>
.team-list {
min-height: 100vh;
background-color: #f5f7fa;
}
/* 成员卡片样式 - 更紧凑 */
.member-card {
background: #ffffff;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.member-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.member-index {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: #e5e7eb;
color: #4b5563;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.member-mobile {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.member-level {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.member-relation {
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.member-direct {
background: #d1fae5;
color: #065f46;
}
.member-indirect {
background: #dbeafe;
color: #1e40af;
}
.member-time {
font-size: 12px;
color: #6b7280;
margin-bottom: 10px;
}
.member-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 10px;
}
.member-stat-item {
text-align: center;
padding: 8px;
background: #f9fafb;
border-radius: 6px;
}
.member-stat-label {
font-size: 11px;
color: #6b7280;
margin-bottom: 4px;
}
.member-stat-value {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.member-action {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.detail-btn {
display: inline-flex;
align-items: center;
padding: 6px 16px;
font-size: 13px;
background: #3b82f6;
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.detail-btn:active {
background: #2563eb;
}
.team-member-item {
transition: opacity 0.2s;
}
.team-member-item:active {
opacity: 0.8;
}
/* 搜索框样式 */
.search-box {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
:deep(.van-field) {
background: #ffffff;
border-radius: 6px;
padding: 8px 12px;
}
:deep(.van-field__control) {
font-size: 14px;
color: #1f2937;
}
.search-btn {
background: #3b82f6;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 13px;
margin-left: 8px;
}
.search-btn:active {
background: #2563eb;
}
</style>

View File

@@ -0,0 +1,573 @@
<template>
<div class="upgrade-subordinate-page">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<h1 class="page-title">调整下级级别</h1>
<p class="page-desc">钻石代理可以将团队中的普通代理升级为黄金代理</p>
</div>
</div>
<!-- 说明卡片 -->
<div class="info-card">
<div class="info-content">
<van-icon name="info-o" class="info-icon" />
<div class="info-text">
<p class="info-title">调整说明</p>
<ul class="info-list">
<li>仅可升级普通代理为黄金代理</li>
<li>调整操作免费无需支付费用</li>
</ul>
</div>
</div>
</div>
<!-- 搜索框 -->
<div class="search-section">
<van-field v-model="searchMobile" placeholder="请输入手机号搜索" clearable @clear="handleClear"
@keyup.enter="handleSearch">
<template #button>
<van-button size="small" type="primary" @click="handleSearch" class="search-btn">搜索</van-button>
</template>
</van-field>
</div>
<!-- 代理列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div class="list-container">
<div v-if="teamMembers.length === 0 && !loading" class="empty-state">
<van-icon name="user-o" class="empty-icon" />
<p class="empty-text">暂无可调整的普通代理</p>
</div>
<div v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="member-card">
<div class="member-header">
<div class="member-index">{{ index + 1 }}</div>
<div class="member-info">
<div class="member-mobile">{{ item.mobile || '未绑定手机' }}</div>
<div class="member-time">加入时间{{ formatTime(item.create_time) }}</div>
</div>
<div class="member-badge">
<span class="level-badge level-normal">普通代理</span>
</div>
</div>
<div class="member-stats">
<div class="stat-item">
<div class="stat-label">总查询量</div>
<div class="stat-value">{{ formatCount(item.total_queries || 0) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">返佣总额</div>
<div class="stat-value">¥{{ formatNumber(item.total_rebate_amount || 0) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">邀请人数</div>
<div class="stat-value">{{ formatCount(item.total_invites || 0) }}</div>
</div>
</div>
<div class="member-action">
<van-button type="primary" size="small" :loading="item.upgrading"
@click="handleUpgrade(item)">
<van-icon name="arrow-up" class="mr-1" />
调整为黄金代理
</van-button>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
<!-- 确认升级弹窗 -->
<van-popup v-model:show="showConfirmDialog" round position="center"
:style="{ width: '85%', maxWidth: '400px' }">
<div class="confirm-dialog">
<div class="confirm-header">
<van-icon name="warning-o" class="warning-icon" />
<h3 class="confirm-title">确认升级</h3>
</div>
<div class="confirm-content">
<p class="confirm-text">
确定要将 <span class="highlight">{{ currentUpgradeItem?.mobile }}</span> 升级为黄金代理吗
</p>
<div class="confirm-notice">
<p> 升级操作不可撤销</p>
<p> 升级操作免费</p>
</div>
</div>
<div class="confirm-actions">
<van-button plain @click="showConfirmDialog = false">取消</van-button>
<van-button type="primary" :loading="isUpgrading" @click="confirmUpgrade">确认升级</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeMount, computed } from 'vue'
import { useAgentStore } from '@/stores/agentStore'
import { getTeamList, upgradeSubordinate } from '@/api/agent'
import { showToast, showSuccessToast, showFailToast } from 'vant'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
const router = useRouter()
const agentStore = useAgentStore()
const { isAgent, level } = storeToRefs(agentStore)
const teamMembers = ref([])
const loading = ref(false)
const finished = ref(false)
const page = ref(1)
const pageSize = 10
const refreshing = ref(false)
const searchMobile = ref('')
const showConfirmDialog = ref(false)
const currentUpgradeItem = ref(null)
const isUpgrading = ref(false)
// 检查是否是钻石代理
const isDiamondAgent = computed(() => {
return isAgent.value && level.value === 3
})
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await fetchTeamMembers()
}
}
onBeforeMount(() => {
page.value = 1
finished.value = false
})
// 获取团队列表(仅普通代理)
const fetchTeamMembers = async () => {
if (loading.value || finished.value) return
loading.value = true
try {
const params = {
page: page.value,
page_size: pageSize
}
if (searchMobile.value && searchMobile.value.trim()) {
params.mobile = searchMobile.value.trim()
}
const { data, error } = await getTeamList(params)
if (data.value && !error.value) {
if (data.value.code === 200) {
let list = data.value.data.list || []
// 过滤出普通代理level === 1
list = list.filter(item => {
const agentLevel = item.level || (item.level_name === '普通' ? 1 : item.level_name === '黄金' ? 2 : item.level_name === '钻石' ? 3 : 1)
return agentLevel === 1
})
if (page.value === 1) {
teamMembers.value = list
} else {
teamMembers.value.push(...list)
}
// 判断是否加载完成
if (list.length < pageSize) {
finished.value = true
} else if (teamMembers.value.length >= data.value.data.total) {
finished.value = true
} else {
page.value++
}
} else {
// 接口返回错误,停止翻页
finished.value = true
showFailToast(data.value.msg || '获取团队列表失败')
}
} else {
// 请求失败或返回错误,停止翻页
finished.value = true
showFailToast('获取团队列表失败')
console.error('获取团队列表失败:', error.value || '请求失败')
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取团队列表失败:', err)
showFailToast('获取团队列表失败')
} finally {
loading.value = false
}
}
// 下拉刷新
const onRefresh = () => {
finished.value = false
page.value = 1
fetchTeamMembers().finally(() => {
refreshing.value = false
})
}
// 搜索功能
const handleSearch = () => {
finished.value = false
page.value = 1
fetchTeamMembers()
}
// 清空搜索
const handleClear = () => {
searchMobile.value = ''
finished.value = false
page.value = 1
fetchTeamMembers()
}
// 处理升级
const handleUpgrade = (item) => {
currentUpgradeItem.value = item
showConfirmDialog.value = true
}
// 确认升级
const confirmUpgrade = async () => {
if (!currentUpgradeItem.value) return
isUpgrading.value = true
try {
const { data, error } = await upgradeSubordinate({
subordinate_id: currentUpgradeItem.value.agent_id || currentUpgradeItem.value.id,
to_level: 2 // 只能升级为黄金代理
})
if (data.value && !error.value) {
if (data.value.code === 200) {
showSuccessToast('升级成功')
showConfirmDialog.value = false
currentUpgradeItem.value = null
// 刷新列表
finished.value = false
page.value = 1
await fetchTeamMembers()
// 刷新代理状态
await agentStore.fetchAgentStatus()
} else {
showFailToast(data.value.msg || '升级失败')
}
} else {
showFailToast('升级失败,请重试')
}
} catch (err) {
console.error('升级失败:', err)
showFailToast('升级失败,请重试')
} finally {
isUpgrading.value = false
}
}
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return '-'
return timeStr.split(' ')[0]
}
// 格式化金额
const formatNumber = (num) => {
if (!num) return '0.00'
return Number(num).toFixed(2)
}
// 格式化数字
const formatCount = (num) => {
if (!num) return '0'
return Number(num).toLocaleString()
}
onMounted(() => {
// 检查权限
if (!isDiamondAgent.value) {
showFailToast('只有钻石代理可以升级下级')
router.back()
return
}
fetchTeamMembers()
})
</script>
<style scoped>
.upgrade-subordinate-page {
min-height: 100vh;
background-color: #f5f7fa;
padding-bottom: 20px;
}
/* 页面头部 */
.page-header {
background: linear-gradient(120deg, #34c9ad 70%, #64d2ff 100%);
padding: 20px 16px;
color: white;
}
.header-content {
max-width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.page-desc {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
/* 说明卡片 */
.info-card {
margin: 16px;
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.info-content {
display: flex;
gap: 12px;
}
.info-icon {
font-size: 24px;
color: #1677ff;
flex-shrink: 0;
margin-top: 2px;
}
.info-text {
flex: 1;
}
.info-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.info-list {
margin: 0;
padding-left: 20px;
color: #6b7280;
font-size: 14px;
line-height: 1.8;
}
.info-list li {
margin-bottom: 4px;
}
/* 搜索区域 */
.search-section {
margin: 0 16px 16px;
}
.search-btn {
margin-left: 8px;
}
/* 列表容器 */
.list-container {
padding: 0 16px;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
margin: 0;
}
/* 成员卡片 */
.member-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.member-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.member-index {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: #e5e7eb;
color: #4b5563;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.member-info {
flex: 1;
min-width: 0;
}
.member-mobile {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 4px;
}
.member-time {
font-size: 12px;
color: #6b7280;
}
.member-badge {
flex-shrink: 0;
}
.level-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.level-normal {
background: #e5e7eb;
color: #4b5563;
}
.member-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background: #f9fafb;
border-radius: 8px;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.member-action {
display: flex;
justify-content: flex-end;
}
.mr-1 {
margin-right: 4px;
}
/* 确认弹窗 */
.confirm-dialog {
padding: 24px;
}
.confirm-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.warning-icon {
font-size: 24px;
color: #f59e0b;
}
.confirm-title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.confirm-content {
margin-bottom: 24px;
}
.confirm-text {
font-size: 15px;
color: #374151;
line-height: 1.6;
margin-bottom: 16px;
}
.highlight {
color: #1677ff;
font-weight: 600;
}
.confirm-notice {
background: #fef3c7;
border-radius: 8px;
padding: 12px;
font-size: 13px;
color: #92400e;
line-height: 1.6;
}
.confirm-notice p {
margin: 4px 0;
}
.confirm-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.confirm-actions .van-button {
flex: 1;
max-width: 120px;
}
</style>

View File

@@ -206,7 +206,7 @@ const companyName = import.meta.env.VITE_COMPANY_NAME
<!-- 第八部分 -->
<div class="mb-2 font-bold leading-relaxed">违约</div>
<div class="leading-relaxed">
用户不得利用本服务进行任何损害海南省学宇思网络科技有限公司及其他第三方权益的行为否则海南省学宇思网络科技有限公司有权立即终止为该用户提供本服务并要求用户赔偿损失由此产生的任何后果由用户自行承担与海南省学宇思网络科技有限公司无关
用户不得利用本服务进行任何损害海南海宇大数据有限公司及其他第三方权益的行为否则海南海宇大数据有限公司有权立即终止为该用户提供本服务并要求用户赔偿损失由此产生的任何后果由用户自行承担与海南海宇大数据有限公司无关
</div>
</div>

View File

@@ -138,15 +138,15 @@
<span class="font-semibold" style="color: var(--van-text-color);">¥{{ amount }}</span>
</div>
<div class="flex justify-between items-center">
<span style="color: var(--van-text-color-2);">税收</span>
<span style="color: var(--van-text-color-2);">预估税费</span>
<span class="font-semibold text-red-500">
-¥{{ taxAmount.toFixed(2) }}
-¥{{ estimatedTaxAmount.toFixed(2) }}
</span>
</div>
<div class="border-t pt-2" style="border-color: var(--van-border-color);">
<div class="flex justify-between items-center">
<span class="font-medium" style="color: var(--van-text-color);">实际到账</span>
<span class="font-bold text-lg" style="color: var(--van-theme-primary);">¥{{ actualAmount.toFixed(2) }}</span>
<span class="font-medium" style="color: var(--van-text-color);">预估到账</span>
<span class="font-bold text-lg" style="color: var(--van-theme-primary);">¥{{ estimatedActualAmount.toFixed(2) }}</span>
</div>
</div>
</div>
@@ -261,6 +261,7 @@
import { ref, computed } from "vue";
import { showToast } from "vant";
import RealNameAuthDialog from "@/components/RealNameAuthDialog.vue";
import { applyWithdrawal, getRevenueInfo } from "@/api/agent";
const agentStore = useAgentStore();
const dialogStore = useDialogStore();
@@ -271,24 +272,21 @@ const isSubmitting = ref(false);
const showStatusPopup = ref(false);
const showTaxConfirmPopup = ref(false);
// 税
const taxFreeAmount = ref(0); // 免税额度
const usedExemptionAmount = ref(0); // 已使用免税额度
const remainingExemptionAmount = ref(0); // 剩余免税额度
const taxRate = ref(0); // 税率
// 税费信息(新系统由后端自动计算)
const taxAmount = ref(0);
const actualAmount = ref(0);
// 计算扣税金额和实际到账金额
const taxAmount = computed(() => {
// 计算预估税费和实际到账金额(前端仅作展示,实际由后端计算)
const estimatedTaxAmount = computed(() => {
if (!amount.value) return 0;
const withdrawAmount = Number(amount.value);
// 统一收取6%手续费,不再有免税额度
// 预估税费6%(实际以后端计算为准)
return withdrawAmount * 0.06;
});
const actualAmount = computed(() => {
const estimatedActualAmount = computed(() => {
if (!amount.value) return 0;
return Number(amount.value) - taxAmount.value;
return Number(amount.value) - estimatedTaxAmount.value;
});
// 样式配置
@@ -334,18 +332,15 @@ const availableAmount = ref(null);
const realName = ref("");
const getData = async () => {
const { data: res, error } = await useApiFetch("/agent/revenue")
.get()
.json();
const { data, error } = await getRevenueInfo();
if (res.value?.code === 200 && !error.value) {
availableAmount.value = res.value.data.balance;
if (data.value?.code === 200 && !error.value) {
availableAmount.value = data.value.data.balance;
}
};
onBeforeMount(() => {
getData();
getTax();
});
// 表单验证
@@ -392,18 +387,7 @@ const openRealNameAuth = () => {
dialogStore.openRealNameAuth();
};
// 获取税务
const getTax = async () => {
const { data, error } = await useApiFetch("/agent/withdrawal/tax/exemption")
.get()
.json();
if (data.value?.code === 200 && !error.value) {
taxFreeAmount.value = data.value.data.total_exemption_amount;
usedExemptionAmount.value = data.value.data.used_exemption_amount;
remainingExemptionAmount.value = data.value.data.remaining_exemption_amount;
taxRate.value = data.value.data.tax_rate;
}
};
// 注意:新系统中税费由后端自动计算,无需前端调用税务豁免接口
const handleSubmit = async () => {
// 检查实名认证状态
@@ -424,22 +408,22 @@ const handleSubmit = async () => {
const confirmWithdraw = async () => {
isSubmitting.value = true;
try {
const { data, error } = await useApiFetch("/agent/withdrawal")
.post({
payee_account: alipayAccount.value,
amount: amount.value,
payee_name: realName.value,
})
.json();
if (data.value?.code === 200) {
status.value = data.value.data.status;
const { applyWithdrawal } = await import('@/api/agent');
const { data, error } = await applyWithdrawal({
payee_account: alipayAccount.value,
amount: Number(amount.value),
payee_name: realName.value,
});
if (data.value?.code === 200 && !error.value) {
status.value = 1; // 新系统申请后状态为1待审核
showTaxConfirmPopup.value = false;
showStatusPopup.value = true;
if (status.value === 3) {
failMsg.value = data.value.data.fail_msg;
}
} else {
showToast(data.value?.msg || '提现申请失败,请重试');
}
} catch {
} catch (err) {
console.error('提现申请失败:', err);
showToast('提现申请失败,请重试');
} finally {
isSubmitting.value = false;
}

View File

@@ -1,41 +1,37 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- 提现记录列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div
v-for="(item, index) in data.list"
:key="index"
class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm"
>
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{
item.create_time || "-"
}}</span>
<span class="font-bold" :class="getAmountColor(item.status)"
>{{ item.amount.toFixed(2) }}</span
>
}}</span>
<span class="font-bold" :class="getAmountColor(item.status)">{{ item.amount.toFixed(2) }}</span>
</div>
<div class="flex items-center mb-2">
<span
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getStatusStyle(item.status)"
>
<span
class="w-2 h-2 rounded-full mr-1"
:class="getDotColor(item.status)"
></span>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getStatusStyle(item.status)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.status)"></span>
{{ statusToChinese(item.status) }}
</span>
</div>
<div class="text-xs text-gray-500">
<div class="text-xs text-gray-500 space-y-1">
<div v-if="item.payee_account">
收款账户{{ maskName(item.payee_account) }}
</div>
<div v-if="item.payee_name">
收款人{{ item.payee_name }}
</div>
<div v-if="item.tax_amount > 0">
税费-¥{{ item.tax_amount.toFixed(2) }}
</div>
<div v-if="item.actual_amount > 0" class="text-green-600 font-medium">
实际到账¥{{ item.actual_amount.toFixed(2) }}
</div>
<div v-if="item.withdrawal_no">
提现单号{{ item.withdrawal_no }}
</div>
<div v-if="item.remark">备注{{ item.remark }}</div>
</div>
</div>
@@ -44,10 +40,12 @@
</template>
<script setup>
// 状态映射配置
import { getWithdrawalList } from '@/api/agent'
// 新系统状态映射配置1=待审核2=审核通过3=审核拒绝4=提现中5=提现成功6=提现失败
const statusConfig = {
1: {
chinese: "处理中",
chinese: "待审核",
color: {
bg: "bg-yellow-100",
text: "text-yellow-800",
@@ -56,6 +54,33 @@ const statusConfig = {
},
},
2: {
chinese: "审核通过",
color: {
bg: "bg-blue-100",
text: "text-blue-800",
dot: "bg-blue-500",
amount: "text-blue-500",
},
},
3: {
chinese: "审核拒绝",
color: {
bg: "bg-red-100",
text: "text-red-800",
dot: "bg-red-500",
amount: "text-red-500",
},
},
4: {
chinese: "提现中",
color: {
bg: "bg-purple-100",
text: "text-purple-800",
dot: "bg-purple-500",
amount: "text-purple-500",
},
},
5: {
chinese: "提现成功",
color: {
bg: "bg-green-100",
@@ -64,7 +89,7 @@ const statusConfig = {
amount: "text-green-500",
},
},
3: {
6: {
chinese: "提现失败",
color: {
bg: "bg-red-100",
@@ -99,9 +124,8 @@ const statusToChinese = (status) => {
// 获取状态样式
const getStatusStyle = (status) => {
const config = statusConfig[status] || {};
return `${config.color?.bg || "bg-gray-100"} ${
config.color?.text || "text-gray-800"
}`;
return `${config.color?.bg || "bg-gray-100"} ${config.color?.text || "text-gray-800"
}`;
};
// 获取小圆点颜色
@@ -121,20 +145,23 @@ const onLoad = async () => {
}
};
// 获取数据(修改分页逻辑)
// 获取数据
const getData = async () => {
if (loading.value || finished.value) return
try {
loading.value = true;
const { data: res, error } = await useApiFetch(
`/agent/withdrawal?page=${page.value}&page_size=${pageSize.value}`
)
.get()
.json();
const { data: res, error } = await getWithdrawalList({
page: page.value,
page_size: pageSize.value
});
if (res.value?.code === 200 && !error.value) {
// 保留首次加载数据
// 首次加载
if (page.value === 1) {
data.value = res.value.data;
} else {
// 分页加载
data.value.list.push(...res.value.data.list);
}
@@ -148,7 +175,15 @@ const getData = async () => {
) {
finished.value = true;
}
} else {
// 接口返回错误或请求失败,停止翻页
finished.value = true;
console.error('获取提现列表失败:', res.value?.msg || error.value || '未知错误');
}
} catch (err) {
// 捕获异常,停止翻页
finished.value = true;
console.error('获取提现列表失败:', err);
} finally {
loading.value = false;
}

View File

@@ -2,6 +2,7 @@
const router = useRouter();
import { storeToRefs } from "pinia";
import SectionTitle from "@/components/SectionTitle.vue";
import { showToast } from "vant";
const agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore);
@@ -13,7 +14,15 @@ import housekeepingRiskIcon from "@/assets/images/index/housekeeping_risk_bg.png
import preLoanRiskIcon from "@/assets/images/index/preloan_risk_bg.png";
import { toArray } from "lodash";
function toInquire(name) {
router.push(`/inquire/${name}`);
// 新系统:普通查询已禁用,需要成为代理或通过推广链接访问
// 如果是代理,跳转到推广页面生成推广链接
if (isAgent.value) {
router.push({ name: "agentPromote", query: { feature: name } });
} else {
// 不是代理,提示并引导到注册页面(可以输入邀请码申请成为代理)
showToast({ message: "请先成为代理才能使用查询功能" });
router.push("/register");
}
}
function toInvitation() {
router.push({ name: "invitation" });
@@ -39,8 +48,8 @@ const toAgent = () => {
const services = ref([
{
title: "个人大数据",
name: "personalData",
title: "个人风险",
name: "riskassessment",
subtitle: "数据洞察,规避风险",
bg: personalDataIcon,
goColor: "#6699ff",
@@ -90,7 +99,7 @@ const noticeText = ref([]);
// 报告类型列表
const reportTypes = [
"个人大数据",
"个人风险",
"婚恋风险",
"入职背调",
"家政风险",
@@ -211,7 +220,7 @@ const toBigData = () => {
</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">查询记录有效期为{{ useAppStore().queryRetentionDays || 30 }}</div>
</div>
<img src="@/assets/images/index/right.png" alt="右箭头" class="h-6 w-6" />
</div>

View File

@@ -16,14 +16,15 @@ export default defineConfig({
proxy: {
"/api/v1": {
target: "http://127.0.0.1:8888", // 本地接口地址
// target: "https://onecha.cn", // 本地接口地址
changeOrigin: true,
},
},
},
build: {
// 构建优化
target: 'es2015', // 支持更多浏览器
minify: 'terser', // 使用terser进行压缩
target: "es2015", // 支持更多浏览器
minify: "terser", // 使用terser进行压缩
terserOptions: {
compress: {
drop_console: true, // 移除console.log
@@ -34,15 +35,15 @@ export default defineConfig({
output: {
// 代码分割策略
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
vant: ['vant'],
utils: ['axios', 'lodash', 'crypto-js'],
charts: ['echarts', 'vue-echarts'],
vendor: ["vue", "vue-router", "pinia"],
vant: ["vant"],
utils: ["axios", "lodash", "crypto-js"],
charts: ["echarts", "vue-echarts"],
},
// 文件名策略
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
chunkFileNames: "assets/js/[name]-[hash].js",
entryFileNames: "assets/js/[name]-[hash].js",
assetFileNames: "assets/[ext]/[name]-[hash].[ext]",
},
},
// 启用CSS代码分割
@@ -59,11 +60,7 @@ export default defineConfig({
"@vueuse/core", // 自动引入 VueUse 中的工具函数(可选)
],
dts: "src/auto-imports.d.ts", // 生成类型定义文件(可选)
dirs: [
"src/composables",
"src/stores",
"src/components",
],
dirs: ["src/composables", "src/stores", "src/components"],
resolvers: [VantResolver()],
}),
Components({
@@ -79,6 +76,6 @@ export default defineConfig({
},
// 优化依赖预构建
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'vant', 'axios'],
include: ["vue", "vue-router", "pinia", "vant", "axios"],
},
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,256 @@
# 微信 H5 授权流程重构 - 设计文档
## 📋 核心业务需求
在微信 H5 环境中,**整个应用在没有有效 token 时,都需要进行微信授权登录**。授权完成后,用户应该被重定向回他们尝试访问的原始页面。
---
## 🔄 流程设计
### **完整的授权流程图**
```
┌─────────────────────────────────────────────────────────────┐
│ 用户在微信中访问任意页面(无 token
└──────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 路由守卫检测:微信 + 无 token │
│ → 保存目标路由到 authStore.pendingRoute │
│ → 生成微信授权 URL │
│ → window.location.href = 授权 URL (不调用 next()
└──────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 跳转到微信授权页面 │
│ https://open.weixin.qq.com/connect/oauth2/authorize?... │
└──────────────────┬──────────────────────────────────────────┘
┌─────────┴─────────┐
│ 用户点击"同意" │
└────────┬──────────┘
┌─────────────────────────────────────────────────────────────┐
│ 微信回调redirectUri?code=xxx&state=yyy │
│ 浏览器重新加载应用URL 中包含 code/state 参数 │
└──────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ App.vue onMounted 检测到 code + state 参数 │
│ → 调用 handleWeixinAuthCallback(code) │
└──────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 1. 调用后端接口 /user/wxh5Auth 交换 token │
│ 2. 保存 token 到 localStorage │
│ 3. 清理 URL 中的 code/state 参数 │
│ 4. 获取用户信息 │
│ 5. 标记授权完成 authStore.completeWeixinAuth() │
└──────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 获取 pendingRoute重定向回原始页面 │
│ router.replace(pendingRoute) │
│ 如果没有 pendingRoute重定向到首页 "/" │
└──────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ✅ 授权完成,用户看到原始页面 │
└─────────────────────────────────────────────────────────────┘
```
---
## 📁 关键文件修改
### **1. `src/router/index.js` - 路由守卫**
**关键变化:**
- 检测到 **微信 + 无 token** 的情况
- **直接发起授权**(调用 `window.location.href`
- **不调用 `next()`**,完全停止导航
- 授权 URL 中的 redirectUri 指向当前页面(清理旧的 code/state 参数)
**代码片段:**
```javascript
if (isWeChat.value && !isAuthenticated && !isTokenExpired) {
// 保存目标路由
authStore.startWeixinAuth(to);
// 生成授权 URL
const appId = import.meta.env.VITE_WECHAT_APP_ID;
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("code");
params.delete("state");
const cleanUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""}`;
const redirectUri = encodeURIComponent(cleanUrl);
const weixinAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=snsapi_base#wechat_redirect`;
// 直接跳转,不调用 next()
window.location.href = weixinAuthUrl;
return;
}
```
### **2. `src/App.vue` - 授权回调处理**
**关键职责:**
1. **检测回调**:读取 URL 中的 code/state 参数
2. **处理回调**:调用 `/user/wxh5Auth` 交换 token
3. **恢复导航**:使用保存的 pendingRoute 重定向用户
**核心函数:`handleWeixinAuthCallback(code)`**
```javascript
const handleWeixinAuthCallback = async (code) => {
// 1. 交换 token
const { data, error } = await useApiFetch("/user/wxh5Auth").post({ code }).json();
// 2. 保存 token
localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem("refreshAfter", data.value.data.refreshAfter);
localStorage.setItem("accessExpire", data.value.data.accessExpire);
// 3. 清理 URL
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("code");
params.delete("state");
window.history.replaceState({}, "", newUrl);
// 4. 获取用户信息
await userStore.fetchUserInfo();
// 5. 标记授权完成
authStore.completeWeixinAuth();
// 6. 重定向回 pendingRoute
const pendingRoute = authStore.pendingRoute;
await router.replace(pendingRoute || "/");
};
```
### **3. `src/stores/authStore.js` - 授权状态管理**
**关键方法:**
| 方法 | 作用 |
|------|------|
| `startWeixinAuth(targetRoute)` | 开始授权,保存目标路由 |
| `completeWeixinAuth()` | 标记授权完成 |
| `clearPendingRoute()` | 清除待处理路由 |
| `resetAuthState()` | 重置所有授权状态 |
| `restoreFromStorage()` | 页面刷新后恢复状态 |
---
## 🎯 核心特性
### **1. 同步流程控制**
-**不使用** `watch` 监听 state 的变化
-**不使用** 异步的 route guard + next() 来触发授权
-**直接** 在 route guard 中调用 `window.location.href` 发起授权
**为什么?**
- Watch 是异步的,可能被路由完成打断
- next() 后可能路由已经切换,导致授权流程混乱
- 直接跳转 URL 更加可靠和同步
### **2. 状态持久化**
- 授权开始时保存到 localStorage
- 页面刷新时自动恢复状态
- 防止超时30秒导致的无限授权循环
### **3. URL 参数清理**
- 每次发起授权前,都清理 URL 中旧的 code/state 参数
- 防止参数被重复编码进 redirectUri
- 微信回调时才注入新的 code/state
### **4. 目标页面恢复**
- 路由守卫保存用户尝试访问的页面
- 授权完成后自动重定向到该页面
- 用户无感知的完整授权体验
---
## 🔐 场景处理
### **场景 1无 token + 访问开放页面**
```
用户访问 "/" → 微信检测无 token → 发起授权 → 授权完成后回到首页
```
### **场景 2无 token + 访问推广链接**
```
用户访问 "/agent/promotionInquire/abc123"
→ 微信检测无 token
→ 保存 pendingRoute = "/agent/promotionInquire/abc123"
→ 发起授权
→ 授权完成后回到 "/agent/promotionInquire/abc123"
```
### **场景 3有效 token + 访问任意页面**
```
用户有 token 且未过期 → 路由守卫放行 → 直接加载页面,无授权
```
### **场景 4Token 过期 + 访问需登录页面**
```
用户在非微信环境 → 检测到 token 过期 → 跳转登录页面
```
### **场景 5授权超时**
```
用户授权流程中30 秒内未完成 → 自动重置状态 → 页面刷新时重新授权
```
---
## 🐛 常见问题排查
### **Q1为什么授权后没有跳到原页面**
**A** 检查 authStore 中是否有 pendingRoute
```javascript
console.log('pendingRoute:', authStore.pendingRoute);
```
### **Q2为什么一直在授权页面循环**
**A** 可能是授权回调处理失败。检查:
1. 后端 `/user/wxh5Auth` 接口是否正确返回 token
2. Token 是否正确保存到 localStorage
3. URL 中的 code/state 是否正确清理
### **Q3为什么刷新页面后授权状态丢失**
**A** authStore.restoreFromStorage() 应该在 onMounted 中被调用。检查:
```javascript
authStore.restoreFromStorage(); // 这行很重要!
```
### **Q4在 PC 上测试,为什么无法触发授权?**
**A** 授权只在微信环境中触发isWeChat.value === true。在 PC 上:
- 访问需登录的页面 → 跳转登录页
- 访问开放页面 → 正常加载
---
## 📝 总结
这个重构的核心思想是:
1. **路由守卫 = 决策者**:检测到需要授权就立即发起
2. **App.vue = 处理器**:处理授权回调和状态恢复
3. **AuthStore = 状态管理**:保存和恢复授权相关的状态
4. **同步流程 = 高可靠性**:避免异步竞态条件
通过这个设计,微信授权流程变得:
- ✅ 清晰易懂
- ✅ 可靠稳定
- ✅ 易于测试和调试
- ✅ 完整的用户体验

View File

@@ -0,0 +1,121 @@
# 微信授权重定向问题分析与修复
## 问题描述
微信授权回调成功后,虽然 `pendingRoute` 有正确的值,回调成功也能跳转到应该到达的页面(如 `promotionInquire`),但在加载该页面后,又会自动跳转到首页。
## 根本原因分析
### 问题 1: pendingRoute 清除时序错误 ❌
**位置**: `src/App.vue` 第 69-72 行
**原始代码**:
```javascript
if (pendingRoute) {
authStore.clearPendingRoute(); // ❌ 先清除
await router.replace(pendingRoute); // 用已清空的 null 值跳转
}
```
**问题**
- `clearPendingRoute()``authStore.pendingRoute` 设为 `null`
- 之后 `router.replace(pendingRoute)` 使用的是已清空的 `null`
- 导致实际跳转到 `undefined` 而非目标路由
**修复**: 先跳转再清除 ✅
### 问题 2: 路由守卫重复授权防护缺失 ❌
**位置**: `src/router/index.js` 第 428-444 行
**问题**
- 当用户在授权完成后,路由守卫再次执行时
- 如果 `isWeChat.value && !isAuthenticated` 条件满足
- 会重新触发授权流程,导致页面跳转
**原因**:
- `authStore.isWeixinAuthing``authStore.weixinAuthComplete` 状态未被路由守卫检查
- 虽然 token 已保存,但守卫仍可能因某些时序问题再次触发授权
**修复**: 在路由守卫中增加授权状态检查 ✅
### 问题 3: localStorage 同步问题
**位置**: `src/stores/authStore.js`
**问题**
- 清除 `pendingRoute`可能只清除内存状态localStorage 可能残留数据
- 页面刷新时恢复的数据可能不完整
**修复**: 确保内存和 localStorage 同步清除 ✅
## 修复清单
### ✅ 修复 1: App.vue 中的时序问题
```javascript
if (pendingRoute) {
// ✅ 先跳转
await router.replace(pendingRoute);
// ✅ 再清除
authStore.clearPendingRoute();
}
```
### ✅ 修复 2: 路由守卫的防护检查
在路由守卫中增加:
```javascript
if (isWeChat.value && !isAuthenticated && !isTokenExpired) {
// ✨ 新增:检查是否正在授权或已完成授权
if (authStore.isWeixinAuthing || authStore.weixinAuthComplete) {
console.warn("⚠️ WeChat auth already in progress or completed");
NProgress.done();
next();
return;
}
// ... 继续原有逻辑
}
```
### ✅ 修复 3: authStore 中的日志和同步
增强 `clearPendingRoute()``restoreFromStorage()` 方法的日志记录和错误处理。
### ✅ 修复 4: PromotionInquire.vue 中的延迟处理
添加延迟以确保页面完全加载后再进行任何路由跳转:
```javascript
function isFinishPayment() {
const query = new URLSearchParams(window.location.search);
let orderNo = query.get("out_trade_no");
if (orderNo) {
// ✨ 延迟 100ms 确保页面加载完成
setTimeout(() => {
router.push({ path: "/report", query: { orderNo } });
}, 100);
}
}
```
## 调试建议
### 观察日志的关键点:
1. **微信授权开始**: 看是否出现 "Triggering WeChat auth from route guard"
2. **Token 保存**: 看是否出现 "✅ Token saved successfully"
3. **用户信息加载**: 看 "✅ User info fetched" 是否成功
4. **pendingRoute 获取**: 看 "🎯 pendingRoute:" 后面的值
5. **导航执行**: 看 "🚀 Navigating to pendingRoute:" 和 "✅ Navigated to pendingRoute"
6. **是否重复授权**: 看是否出现 "⚠️ WeChat auth already in progress"
### 测试步骤:
1. 在微信中打开推广链接: `https://xxx.com/agent/promotionInquire/abc123`
2. 观察控制台日志,确保看到上述所有成功日志
3. 验证最终页面是 `promotionInquire` 而非首页
## 修复文件列表
-`src/App.vue` - 修复 pendingRoute 时序问题
-`src/router/index.js` - 增强路由守卫防护
-`src/stores/authStore.js` - 改进日志和同步
-`src/views/PromotionInquire.vue` - 添加延迟处理
## 总结
这个问题是由多个时序和状态管理问题联合造成的:
1. pendingRoute 被过早清除导致跳转失败
2. 路由守卫缺少防护措施可能重复授权
3. 状态同步不完整可能导致恢复失败
通过修复这些问题,应该能够确保微信授权后正确重定向到目标页面。