Compare commits

...

6 Commits

102 changed files with 9655 additions and 4062 deletions

5
.env
View File

@@ -1,7 +1,7 @@
VITE_API_URL= VITE_API_URL=
VITE_API_PREFIX=/api/v1 VITE_API_PREFIX=/api/v1
VITE_COMPANY_NAME=海南省学宇思网络科技有限公司 VITE_COMPANY_NAME=海南海宇大数据有限公司
VITE_INQUIRE_AES_KEY=ff83609b2b24fc73196aac3d3dfb874f VITE_INQUIRE_AES_KEY=ff83609b2b24fc73196aac3d3dfb874f
@@ -13,4 +13,5 @@ VITE_CHAT_AES_IV=345GDFED433223DF
VITE_SHARE_TITLE=一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用 VITE_SHARE_TITLE=一查查|大数据风险报告查询与代理平台,支持个人和企业多场景风控应用
VITE_SHARE_DESC=提供个人信用评估、入职背调、信贷风控、企业风险监测等服务 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> <script setup>
import { RouterLink, RouterView, useRouter } from "vue-router"; import { RouterView, useRouter, useRoute } from "vue-router";
const { isWeChat } = useEnv(); const { isWeChat } = useEnv();
import { useAgentStore } from "@/stores/agentStore"; import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore"; import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from "@/stores/dialogStore"; import { useDialogStore } from "@/stores/dialogStore";
import { useAuthStore } from "@/stores/authStore"; import { useAuthStore } from "@/stores/authStore";
import { useAppStore } from "@/stores/appStore";
import { useWeixinShare } from "@/composables/useWeixinShare"; import { useWeixinShare } from "@/composables/useWeixinShare";
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue";
const router = useRouter(); const router = useRouter();
const route = useRoute();
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();
const dialogStore = useDialogStore(); const dialogStore = useDialogStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const { configWeixinShare, setDynamicShare } = useWeixinShare(); const appStore = useAppStore();
const { setDynamicShare } = useWeixinShare();
onMounted(() => { onMounted(async () => {
// 检查token版本如果版本不匹配则清除旧token // 初始化应用配置
checkTokenVersion() await appStore.fetchAppConfig();
// 恢复微信授权状态(页面刷新 // 恢复微信授权状态(页面刷新或回调时
authStore.restoreFromStorage(); authStore.restoreFromStorage();
// 检查是否是微信授权回调 // 检查是否是微信授权回调
const url = new URL(window.location.href); 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();
}
RefreshToken();
const token = localStorage.getItem("token");
if (token) {
agentStore.fetchAgentStatus();
userStore.fetchUserInfo();
}
// 配置微信分享
// 延迟执行确保微信SDK已加载
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 500);
// 监听路由变化,更新微信分享配置
router.afterEach(() => {
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 300);
});
});
const checkTokenVersion = () => {
// 设置新的token版本号当后端token格式改变时修改这个版本号
const CURRENT_TOKEN_VERSION = '2.1'
const storedTokenVersion = localStorage.getItem('tokenVersion')
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) { if (code && state) {
// 这是微信授权回调,处理授权结果 // 这是微信授权回调,处理授权结果
console.log("Handling WeChat auth callback");
await handleWeixinAuthCallback(code);
} else {
// 正常初始化:加载用户信息等
await initializeApp();
}
getWeixinAuthUrl();
// 延迟配置微信分享
setTimeout(async () => {
if (isWeChat.value && window.jWeixin) {
await setDynamicShare();
}
}, 1000);
// 监听路由变化更新分享配置
router.afterEach(async () => {
if (isWeChat.value && window.jWeixin && !authStore.isWeixinAuthing) {
await setDynamicShare();
}
});
});
/**
* 处理微信授权回调
*/
const handleWeixinAuthCallback = async (code) => {
try {
console.log("🔄 WeChat auth callback: code=", code);
// 调用后端接口交换 token
const { data, error } = await useApiFetch("/user/wxh5Auth") const { data, error } = await useApiFetch("/user/wxh5Auth")
.post({ code }) .post({ code })
.json(); .json();
if (data.value && !error.value) { console.log("📡 wxh5Auth response:", {
if (data.value.code === 200) { code: data.value?.code,
localStorage.setItem("token", data.value.data.accessToken); hasToken: !!data.value?.data?.accessToken,
localStorage.setItem( error: error.value
"refreshAfter", });
data.value.data.refreshAfter
);
localStorage.setItem(
"accessExpire",
data.value.data.accessExpire
);
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);
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}`);
// 验证 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不一致");
}
// 清除 URL 中的 code 和 state 参数
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
params.delete("code"); params.delete("code");
params.delete("state"); params.delete("state");
const newUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""}`;
// 更新 URL不刷新页面
const newUrl = `${url.origin}${url.pathname
}?${params.toString()}`;
window.history.replaceState({}, "", newUrl); window.history.replaceState({}, "", newUrl);
console.log("✅ URL cleaned, removed code and state parameters");
// 标记微信授权完成 // 获取用户信息
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(); authStore.completeWeixinAuth();
console.log("✅ WeChat auth marked as complete");
// 获取用户和代理信息 // 获取 pendingRoute 并跳转
await Promise.all([ const pendingRoute = authStore.pendingRoute
agentStore.fetchAgentStatus(), console.log("🎯 pendingRoute:", pendingRoute);
userStore.fetchUserInfo()
]);
// 如果有待处理的路由,跳转到该路由 // if (pendingRoute) {
if (authStore.pendingRoute) { // // ⚠️ 重要:必须先跳转再清除,否则清除后 pendingRoute 为 null
router.replace(authStore.pendingRoute); // console.log("🚀 Navigating to pendingRoute:", pendingRoute);
authStore.clearPendingRoute(); // 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 { } else {
// 没有授权参数,需要开始微信授权 console.error("❌ WeChat auth failed:", {
// 保存当前路由作为授权完成后的目标路由 code: data.value?.code,
const currentRoute = router.currentRoute.value; message: data.value?.msg || data.value?.message,
authStore.startWeixinAuth(currentRoute); error: error.value
h5WeixinGetCode(); });
// 授权失败,重置状态
authStore.resetAuthState();
// 跳转到登录页
await router.replace("/login");
}
} 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); * 初始化应用:检查 token加载用户信息
let appId = import.meta.env.VITE_WECHAT_APP_ID; */
let state = "snsapi_base"; const initializeApp = async () => {
let scope = "snsapi_base"; // 检查 token 版本
let authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`; checkTokenVersion();
// 跳转到授权URL
window.location.href = authUrl; // 尝试刷新 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> </script>
<template> <template>
<RouterView /> <RouterView />
<BindPhoneDialog /> <BindPhoneDialog />
<BindPhoneOnlyDialog />
</template> </template>
<style scoped></style> <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 axios from "axios";
import useApiFetch from "@/composables/useApiFetch";
// 获取API基础URL与生产规则一致VITE_API_URL // 获取API基础URL与生产规则一致VITE_API_URL
const baseURL = import.meta.env.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 // 注销账号API
export function cancelAccount() { export function cancelAccount() {
return axios({ 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 useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore']
const useAnimate: typeof import('@vueuse/core')['useAnimate'] const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useApiFetch: typeof import('./composables/useApiFetch.js')['default'] const useApiFetch: typeof import('./composables/useApiFetch.js')['default']
const useAppStore: typeof import('./stores/appStore.js')['useAppStore']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']

View File

@@ -6,17 +6,22 @@
> >
成为代理 成为代理
</div> </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"> <div class="p-4">
<van-field
label-width="56"
v-model="form.referrer"
label="邀请信息"
name="referrer"
placeholder="请输入邀请码/代理编码/代理手机号"
required
/>
<van-field <van-field
label-width="56" label-width="56"
v-model="form.region" v-model="form.region"
is-link is-link
readonly readonly
label="地区" label="地区"
placeholder="请选择地区" placeholder="请选择地区(可选)"
@click="showCascader = true" @click="showCascader = true"
/> />
<van-popup v-model:show="showCascader" round position="bottom"> <van-popup v-model:show="showCascader" round position="bottom">
@@ -124,10 +129,6 @@ import { useCascaderAreaData } from "@vant/area-data";
import { showToast } from "vant"; // 引入 showToast 方法 import { showToast } from "vant"; // 引入 showToast 方法
const emit = defineEmits(); // 确保 emit 可以正确使用 const emit = defineEmits(); // 确保 emit 可以正确使用
const props = defineProps({ const props = defineProps({
ancestor: {
type: String,
required: true,
},
isSelf: { isSelf: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -137,11 +138,12 @@ const props = defineProps({
default: "", default: "",
}, },
}); });
const { ancestor, isSelf, userName } = toRefs(props); const { isSelf, userName } = toRefs(props);
const form = ref({ const form = ref({
referrer: "",
region: "", region: "",
mobile: "", mobile: "",
code: "", // 增加验证码字段 code: "", // 验证码字段
}); });
const showCascader = ref(false); const showCascader = ref(false);
const cascaderValue = ref(""); const cascaderValue = ref("");
@@ -207,10 +209,11 @@ onUnmounted(() => {
}); });
const submit = () => { const submit = () => {
// 校验表单字段 // 校验表单字段
if (!form.value.region) { if (!form.value.referrer || !form.value.referrer.trim()) {
showToast({ message: "请选择地区" }); showToast({ message: "请输入邀请信息" });
return; return;
} }
if (!form.value.mobile) { if (!form.value.mobile) {
showToast({ message: "请输入手机号" }); showToast({ message: "请输入手机号" });
return; return;
@@ -231,15 +234,13 @@ const submit = () => {
showToast({ message: "请先阅读并同意用户协议及相关条款" }); showToast({ message: "请先阅读并同意用户协议及相关条款" });
return; 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 = () => { const closePopup = () => {
emit("close"); 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 { splitJRZQ7F1AForTabs } from '@/ui/JRZQ7F1A/utils/simpleSplitter.js';
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js'; import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js'; import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js';
import { useAppStore } from "@/stores/appStore";
// 动态导入产品背景图片的函数 // 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => { const loadProductBackground = async (productType) => {
@@ -17,7 +18,7 @@ const loadProductBackground = async (productType) => {
return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default; return (await import("@/assets/images/report/xwqy_inquire_bg.png")).default;
case 'preloanbackgroundcheck': case 'preloanbackgroundcheck':
return (await import("@/assets/images/report/dqfx_inquire_bg.png")).default; 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; return (await import("@/assets/images/report/grdsj_inquire_bg.png")).default;
case 'marriage': case 'marriage':
return (await import("@/assets/images/report/marriage_inquire_bg.png")).default; 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({ const props = defineProps({
isShare: { isShare: {
type: Boolean, type: Boolean,
@@ -549,7 +552,12 @@ const featureMap = {
component: defineAsyncComponent(() => import("@/ui/YYSY7D3E/index.vue")), component: defineAsyncComponent(() => import("@/ui/YYSY7D3E/index.vue")),
remark: '手机携号转网查询用于检测用户手机号码是否发生过携号转网操作,以及转网前后的运营商信息。携号转网可能影响用户身份验证和信用评估。' remark: '手机携号转网查询用于检测用户手机号码是否发生过携号转网操作,以及转网前后的运营商信息。携号转网可能影响用户身份验证和信用评估。'
}, },
// 借贷意向A
JRZQ6F2A: {
name: "借贷申请",
component: defineAsyncComponent(() => import("@/ui/JRZQ6F2A/index.vue")),
remark: '借贷申请提供全面的借贷申请行为分析,包括申请次数、申请总次数(银行+非银)和申请机构总数(银行+非银)等多维度数据。通过不同时间段的统计分析,全面展示申请人的借贷申请行为。'
},
// 手机在网时长 // 手机在网时长
YYSY8B1C: { YYSY8B1C: {
name: "手机在网时长", name: "手机在网时长",
@@ -638,6 +646,7 @@ const featureRiskLevels = {
'JRZQ0A03': 7, // 借贷申请记录 'JRZQ0A03': 7, // 借贷申请记录
'JRZQ8203': 7, // 借贷行为记录 'JRZQ8203': 7, // 借贷行为记录
'JRZQ4B6C': 7, // 信贷表现 'JRZQ4B6C': 7, // 信贷表现
'JRZQ6F2A': 7, // 借贷申请
'BehaviorRiskScan': 7, // 风险行为扫描 'BehaviorRiskScan': 7, // 风险行为扫描
'IVYZ8I9J': 7, // 网络社交异常 'IVYZ8I9J': 7, // 网络社交异常
'JRZQ8A2D': 9, // 特殊名单验证 'JRZQ8A2D': 9, // 特殊名单验证
@@ -879,7 +888,7 @@ watch([reportData, componentRiskScores], () => {
1本份报告是在取得您个人授权后我们才向合法存有您以上个人信息的机构去调取相关内容我们不会以任何形式对您的报告进行存储除您和您授权的人外不会提供给任何人和机构进行查看 1本份报告是在取得您个人授权后我们才向合法存有您以上个人信息的机构去调取相关内容我们不会以任何形式对您的报告进行存储除您和您授权的人外不会提供给任何人和机构进行查看
</p> </p>
<p class="text-[#999999]"> <p class="text-[#999999]">
&nbsp; &nbsp; 2本报告自生成之日起有效期 30 &nbsp; &nbsp; 2本报告自生成之日起有效期 {{ useAppStore().queryRetentionDays || 30 }}
过期自动删除如果您对本份报告存有异议可能是合作机构数据有延迟或未能获取到您的相关数据出于合作平台数据隐私的保护本平台将不做任何解释 过期自动删除如果您对本份报告存有异议可能是合作机构数据有延迟或未能获取到您的相关数据出于合作平台数据隐私的保护本平台将不做任何解释
</p> </p>
<p class="text-[#999999]"> <p class="text-[#999999]">
@@ -894,17 +903,17 @@ watch([reportData, componentRiskScores], () => {
</div> </div>
<div class="disclaimer"> <div class="disclaimer">
<div class="flex flex-col items-center"> <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="公安备案" /> <img class="w-4 h-4 mr-2" src="@/assets/images/public_security_record_icon.png" alt="公安备案" />
<text>琼公网安备46010002000584号</text> <text>琼公网安备46010002000584号</text>
</div> </div> -->
<div> <div>
<a class="text-blue-500" href="https://beian.miit.gov.cn"> <a class="text-blue-500" href="https://beian.miit.gov.cn">
琼ICP备2024048057-2 琼ICP备2024038584-10
</a> </a>
</div> </div>
</div> </div>
<div>海南省学宇思网络科技有限公司版权所有</div> <div>海南海宇大数据有限公司版权所有</div>
</div> </div>
</template> </template>

View File

@@ -1,33 +1,72 @@
<script setup> <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 { 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 router = useRouter();
const route = useRoute();
const dialogStore = useDialogStore(); const dialogStore = useDialogStore();
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();
const phoneNumber = ref(""); const phoneNumber = ref("");
const verificationCode = ref(""); const verificationCode = ref("");
const inviteCode = ref("");
const isCountingDown = ref(false); const isCountingDown = ref(false);
const countdown = ref(60); const countdown = ref(60);
const isAgreed = ref(false); const isAgreed = ref(false);
const hasAccount = ref(false); // 是否有平台账号
let timer = null; let timer = null;
// 聚焦状态变量 // 聚焦状态变量
const phoneFocused = ref(false); const phoneFocused = ref(false);
const codeFocused = 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(() => { const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value); return /^1[3-9]\d{9}$/.test(phoneNumber.value);
}); });
const canBind = computed(() => { const isInviteCodeValid = computed(() => {
return inviteCode.value.trim().length > 0;
});
const canRegister = computed(() => {
if (hasAccount.value) {
// 已有账号模式:只需要手机号和验证码
return ( return (
isPhoneNumberValid.value && isPhoneNumberValid.value &&
verificationCode.value.length === 6 && verificationCode.value.length === 6 &&
isAgreed.value isAgreed.value
); );
} else {
// 新注册模式:需要手机号、验证码和邀请码
return (
isPhoneNumberValid.value &&
verificationCode.value.length === 6 &&
isInviteCodeValid.value &&
isAgreed.value
);
}
}); });
async function sendVerificationCode() { async function sendVerificationCode() {
@@ -36,8 +75,13 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return; return;
} }
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请先输入邀请码" });
return;
}
const actionType = hasAccount.value ? "bindMobile" : "agentApply";
const { data, error } = await useApiFetch("auth/sendSms") const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile" }) .post({ mobile: phoneNumber.value, actionType })
.json(); .json();
if (data.value && !error.value) { if (data.value && !error.value) {
@@ -46,7 +90,7 @@ async function sendVerificationCode() {
startCountdown(); startCountdown();
// 聚焦到验证码输入框 // 聚焦到验证码输入框
nextTick(() => { nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode'); const verificationCodeInput = document.getElementById('registerVerificationCode');
if (verificationCodeInput) { if (verificationCodeInput) {
verificationCodeInput.focus(); verificationCodeInput.focus();
} }
@@ -70,7 +114,7 @@ function startCountdown() {
}, 1000); }, 1000);
} }
async function handleBind() { async function handleRegister() {
if (!isPhoneNumberValid.value) { if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return; return;
@@ -79,48 +123,117 @@ async function handleBind() {
showToast({ message: "请输入有效的验证码" }); showToast({ message: "请输入有效的验证码" });
return; return;
} }
if (!hasAccount.value && !isInviteCodeValid.value) {
showToast({ message: "请输入邀请码" });
return;
}
if (!isAgreed.value) { if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" }); showToast({ message: "请先同意用户协议" });
return; return;
} }
try {
if (hasAccount.value) {
// 已有账号模式:绑定手机号登录
const { data, error } = await useApiFetch("/user/bindMobile") const { data, error } = await useApiFetch("/user/bindMobile")
.post({ mobile: phoneNumber.value, code: verificationCode.value }) .post({ mobile: phoneNumber.value, code: verificationCode.value })
.json(); .json();
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
showToast({ message: "绑定成功" }); showToast({ message: "绑定成功" });
localStorage.setItem('token', data.value.data.accessToken) localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter) localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire) localStorage.setItem('accessExpire', data.value.data.accessExpire)
closeDialog(); closeDialog();
await Promise.all([ await Promise.all([
agentStore.fetchAgentStatus(), agentStore.fetchAgentStatus(),
userStore.fetchUserInfo() userStore.fetchUserInfo()
]); ]);
// 发出绑定成功的事件 // 发出注册成功的事件
emit('bind-success'); emit('register-success');
// 延迟执行路由检查,确保状态已更新 // 检查是否是代理,如果是代理跳转到代理主页,否则跳转到首页
setTimeout(() => { setTimeout(() => {
// 重新触发路由检查 if (agentStore.isAgent) {
const currentRoute = router.currentRoute.value; router.replace("/agent");
router.replace(currentRoute.path);
}, 100);
} else { } else {
showToast(data.value.msg); router.replace("/");
} }
}, 300);
} else {
// 检查是否是手机号已绑定其他微信的错误
if (data.value.msg && data.value.msg.includes("已绑定其他微信号")) {
showToast({ message: "该手机号已绑定其他微信号,一个微信只能绑定一个手机号" });
} else {
showToast(data.value.msg || "绑定失败,请重试");
}
}
}
} else {
// 新注册模式:通过邀请码注册成为代理
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() { function closeDialog() {
dialogStore.closeBindPhone(); dialogStore.closeRegisterAgent();
// 重置表单 // 重置表单
phoneNumber.value = ""; phoneNumber.value = "";
verificationCode.value = ""; verificationCode.value = "";
inviteCode.value = "";
isAgreed.value = false; isAgreed.value = false;
hasAccount.value = false;
if (timer) { if (timer) {
clearInterval(timer); clearInterval(timer);
} }
@@ -138,17 +251,14 @@ function toPrivacyPolicy() {
</script> </script>
<template> <template>
<div v-if="dialogStore.showBindPhone"> <div v-if="dialogStore.showRegisterAgent">
<van-popup v-model:show="dialogStore.showBindPhone" round position="bottom" :style="{ height: '80%' }" <van-popup v-model:show="dialogStore.showRegisterAgent" round position="bottom" :style="{ height: '85%' }"
@close="closeDialog"> @close="closeDialog">
<div class="bind-phone-dialog"> <div class="register-agent-dialog">
<div class="title-bar"> <div class="title-bar">
<div class="font-bold">绑定手机号码</div> <div class="font-bold">注册成为代理</div>
<div class="text-sm text-gray-500 mt-1"> <div class="text-sm text-gray-500 mt-1">
为使用完整功能请绑定手机号码 {{ hasAccount ? '绑定手机号登录已有账号' : '请输入手机号和邀请码完成代理注册' }}
</div>
<div class="text-sm text-gray-500 mt-1">
如该微信号之前已绑定过手机号请输入已绑定的手机号
</div> </div>
<van-icon name="cross" class="close-icon" @click="closeDialog" /> <van-icon name="cross" class="close-icon" @click="closeDialog" />
</div> </div>
@@ -163,6 +273,48 @@ function toPrivacyPolicy() {
</div> </div>
<div class="space-y-5"> <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="[ <div :class="[
'input-container bg-blue-300/20', 'input-container bg-blue-300/20',
@@ -178,13 +330,13 @@ function toPrivacyPolicy() {
'input-container bg-blue-300/20', 'input-container bg-blue-300/20',
codeFocused ? 'focused' : '', 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" placeholder="请输入验证码" maxlength="6" @focus="codeFocused = true"
@blur="codeFocused = false" /> @blur="codeFocused = false" />
</div> </div>
<button <button
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300" 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' ? 'cursor-not-allowed bg-gray-300 text-gray-500'
: 'bg-blue-500 text-white hover:bg-blue-600' : 'bg-blue-500 text-white hover:bg-blue-600'
" @click="sendVerificationCode"> " @click="sendVerificationCode">
@@ -200,7 +352,7 @@ function toPrivacyPolicy() {
<div class="flex items-start space-x-2"> <div class="flex items-start space-x-2">
<input type="checkbox" v-model="isAgreed" class="mt-1" /> <input type="checkbox" v-model="isAgreed" class="mt-1" />
<span class="text-xs text-gray-400 leading-tight"> <span class="text-xs text-gray-400 leading-tight">
绑定手机号即代表您已阅读并同意 注册成为代理即代表您已阅读并同意
<a class="cursor-pointer text-blue-400" @click="toUserAgreement"> <a class="cursor-pointer text-blue-400" @click="toUserAgreement">
用户协议 用户协议
</a> </a>
@@ -214,8 +366,8 @@ function toPrivacyPolicy() {
<button <button
class="mt-10 w-full py-3 text-lg font-bold text-white bg-blue-500 rounded-full transition duration-300" 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> </button>
</div> </div>
</div> </div>
@@ -224,7 +376,7 @@ function toPrivacyPolicy() {
</template> </template>
<style scoped> <style scoped>
.bind-phone-dialog { .register-agent-dialog {
background: url("@/assets/images/login_bg.png") no-repeat; background: url("@/assets/images/login_bg.png") no-repeat;
background-position: center; background-position: center;
background-size: cover; 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 canvasHeight = 180
const bgImgUrl = '/image/clickCaptcha.jpg' // 可替换为任意背景图 const bgImgUrl = '/image/clickCaptcha.jpg' // 可替换为任意背景图
const allChars = ['大', '数', '据', '全', '能', '查', '风', '险', '报', '告'] const allChars = ['大', '数', '据', '', '查', '风', '险', '报', '告']
const targetChars = ref(['', '', '查']) // 目标点击顺序固定 const targetChars = ref(['', '', '查']) // 目标点击顺序固定
const charPositions = ref([]) // [{char, x, y, w, h}] const charPositions = ref([]) // [{char, x, y, w, h}]
const clickedIndex = ref(0) const clickedIndex = ref(0)
const errorMessage = ref('') const errorMessage = ref('')
@@ -134,7 +134,7 @@ function generateCaptcha() {
;[chars[i], chars[j]] = [chars[j], chars[i]] ;[chars[i], chars[j]] = [chars[j], chars[i]]
} }
currentChars = chars currentChars = chars
targetChars.value = ['', '', '查'] targetChars.value = ['', '', '查']
clickedIndex.value = 0 clickedIndex.value = 0
errorMessage.value = '' errorMessage.value = ''
successMessage.value = '' successMessage.value = ''
@@ -239,9 +239,9 @@ watch(
padding: 1rem; padding: 1rem;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out; animation: fadeIn 0.2s ease-out;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
} }
@@ -249,9 +249,9 @@ watch(
to { to {
opacity: 1; opacity: 1;
} }
} }
.captcha-modal { .captcha-modal {
background: #fff; background: #fff;
border-radius: 1rem; border-radius: 1rem;
width: 100%; width: 100%;
@@ -259,9 +259,9 @@ watch(
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
overflow: hidden; overflow: hidden;
animation: slideUp 0.3s ease-out; animation: slideUp 0.3s ease-out;
} }
@keyframes slideUp { @keyframes slideUp {
from { from {
transform: translateY(20px); transform: translateY(20px);
opacity: 0; opacity: 0;
@@ -271,18 +271,18 @@ watch(
transform: translateY(0); transform: translateY(0);
opacity: 1; opacity: 1;
} }
} }
.captcha-header { .captcha-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--color-border-primary, #ebedf0); border-bottom: 1px solid var(--color-border-primary, #ebedf0);
background: linear-gradient(135deg, var(--color-primary-light, rgba(140, 198, 247, 0.1)), #ffffff); background: linear-gradient(135deg, var(--color-primary-light, rgba(140, 198, 247, 0.1)), #ffffff);
} }
.captcha-title { .captcha-title {
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 600; font-weight: 600;
color: var(--color-text-primary, #323233); color: var(--color-text-primary, #323233);
@@ -291,9 +291,9 @@ watch(
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.close-btn { .close-btn {
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
border: none; border: none;
font-size: 1.5rem; font-size: 1.5rem;
@@ -308,20 +308,20 @@ watch(
justify-content: center; justify-content: center;
transition: all 0.2s ease; transition: all 0.2s ease;
line-height: 1; line-height: 1;
} }
.close-btn:hover { .close-btn:hover {
color: var(--color-text-primary, #323233); color: var(--color-text-primary, #323233);
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
transform: rotate(90deg); transform: rotate(90deg);
} }
.captcha-content { .captcha-content {
padding: 1.5rem 1.5rem 1rem 1.5rem; padding: 1.5rem 1.5rem 1rem 1.5rem;
background: #ffffff; background: #ffffff;
} }
.captcha-canvas { .captcha-canvas {
width: 100%; width: 100%;
border-radius: 0.75rem; border-radius: 0.75rem;
background: var(--color-bg-tertiary, #f8f8f8); background: var(--color-bg-tertiary, #f8f8f8);
@@ -330,26 +330,26 @@ watch(
border: 2px solid var(--color-border-primary, #ebedf0); border: 2px solid var(--color-border-primary, #ebedf0);
transition: all 0.2s ease; transition: all 0.2s ease;
cursor: pointer; cursor: pointer;
} }
.captcha-canvas:hover { .captcha-canvas:hover {
border-color: var(--color-primary, #8CC6F7); border-color: var(--color-primary, #8CC6F7);
box-shadow: 0 0 0 3px var(--color-primary-light, rgba(140, 198, 247, 0.1)); box-shadow: 0 0 0 3px var(--color-primary-light, rgba(140, 198, 247, 0.1));
} }
.captcha-instruction { .captcha-instruction {
margin: 1.25rem 0 0.75rem 0; margin: 1.25rem 0 0.75rem 0;
text-align: center; text-align: center;
} }
.captcha-instruction p { .captcha-instruction p {
font-size: 0.95rem; font-size: 0.95rem;
color: var(--color-text-secondary, #646566); color: var(--color-text-secondary, #646566);
margin: 0; margin: 0;
line-height: 1.5; line-height: 1.5;
} }
.target-list { .target-list {
color: var(--color-primary, #8CC6F7); color: var(--color-primary, #8CC6F7);
font-weight: 600; font-weight: 600;
font-size: 1.05rem; font-size: 1.05rem;
@@ -357,15 +357,15 @@ watch(
background: var(--color-primary-light, rgba(140, 198, 247, 0.1)); background: var(--color-primary-light, rgba(140, 198, 247, 0.1));
border-radius: 0.375rem; border-radius: 0.375rem;
display: inline-block; display: inline-block;
} }
.captcha-status { .captcha-status {
text-align: center; text-align: center;
min-height: 1.75rem; min-height: 1.75rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.error-message { .error-message {
color: var(--color-danger, #ee0a24); color: var(--color-danger, #ee0a24);
font-size: 0.875rem; font-size: 0.875rem;
margin: 0; margin: 0;
@@ -373,9 +373,9 @@ watch(
background: rgba(238, 10, 36, 0.1); background: rgba(238, 10, 36, 0.1);
border-radius: 0.5rem; border-radius: 0.5rem;
animation: shake 0.3s ease-in-out; animation: shake 0.3s ease-in-out;
} }
@keyframes shake { @keyframes shake {
0%, 0%,
100% { 100% {
@@ -389,9 +389,9 @@ watch(
75% { 75% {
transform: translateX(5px); transform: translateX(5px);
} }
} }
.success-message { .success-message {
color: var(--color-success, #07c160); color: var(--color-success, #07c160);
font-size: 0.875rem; font-size: 0.875rem;
margin: 0; margin: 0;
@@ -399,9 +399,9 @@ watch(
background: rgba(7, 193, 96, 0.1); background: rgba(7, 193, 96, 0.1);
border-radius: 0.5rem; border-radius: 0.5rem;
animation: bounce 0.4s ease-out; animation: bounce 0.4s ease-out;
} }
@keyframes bounce { @keyframes bounce {
0%, 0%,
100% { 100% {
@@ -411,24 +411,24 @@ watch(
50% { 50% {
transform: translateY(-3px); transform: translateY(-3px);
} }
} }
.status-text { .status-text {
color: var(--color-text-tertiary, #969799); color: var(--color-text-tertiary, #969799);
font-size: 0.875rem; font-size: 0.875rem;
margin: 0; margin: 0;
} }
.captcha-footer { .captcha-footer {
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
border-top: 1px solid var(--color-border-primary, #ebedf0); border-top: 1px solid var(--color-border-primary, #ebedf0);
background: var(--color-bg-secondary, #fafafa); background: var(--color-bg-secondary, #fafafa);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
} }
.refresh-btn { .refresh-btn {
width: 100%; width: 100%;
padding: 0.875rem; padding: 0.875rem;
background: var(--color-primary, #8CC6F7); background: var(--color-primary, #8CC6F7);
@@ -440,25 +440,25 @@ watch(
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3); box-shadow: 0 2px 8px rgba(140, 198, 247, 0.3);
} }
.refresh-btn:hover:not(:disabled) { .refresh-btn:hover:not(:disabled) {
background: var(--color-primary-dark, rgba(140, 198, 247, 0.8)); background: var(--color-primary-dark, rgba(140, 198, 247, 0.8));
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(140, 198, 247, 0.4); box-shadow: 0 4px 12px rgba(140, 198, 247, 0.4);
} }
.refresh-btn:active:not(:disabled) { .refresh-btn:active:not(:disabled) {
transform: translateY(0); transform: translateY(0);
} }
.refresh-btn:disabled { .refresh-btn:disabled {
background: var(--color-text-disabled, #c8c9cc); background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed; cursor: not-allowed;
box-shadow: none; box-shadow: none;
} }
.confirm-btn { .confirm-btn {
width: 100%; width: 100%;
padding: 0.875rem; padding: 0.875rem;
background: var(--color-success, #07c160); background: var(--color-success, #07c160);
@@ -470,25 +470,25 @@ watch(
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3); box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
} }
.confirm-btn:hover:not(:disabled) { .confirm-btn:hover:not(:disabled) {
background: rgba(7, 193, 96, 0.9); background: rgba(7, 193, 96, 0.9);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4); box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
} }
.confirm-btn:active:not(:disabled) { .confirm-btn:active:not(:disabled) {
transform: translateY(0); transform: translateY(0);
} }
.confirm-btn:disabled { .confirm-btn:disabled {
background: var(--color-text-disabled, #c8c9cc); background: var(--color-text-disabled, #c8c9cc);
cursor: not-allowed; cursor: not-allowed;
box-shadow: none; box-shadow: none;
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.captcha-overlay { .captcha-overlay {
padding: 0.75rem; padding: 0.75rem;
} }

View File

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

View File

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

View File

@@ -16,15 +16,20 @@
</div> </div>
<div class="flex items-center justify-between mt-2"> <div class="flex items-center justify-between mt-2">
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span></div> <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> </div>
<div class="card m-4"> <div class="card m-4">
<div class="text-lg mb-2">收益与成本说明</div> <div class="text-lg mb-2">收益与成本说明</div>
<div>推广收益 = 客户查询价 - 我的成本</div> <div>推广收益 = 客户查询价 - 我的成本</div>
<div>我的成本 = 提价成本 + 价成本</div> <div>我的成本 = 实际底价 + 价成本</div>
<div class="mt-1">提价成本超过平台标准定价部分平台会收取部分成本价</div> <div class="mt-1">提价成本设置{{ productConfig.price_threshold }}元以上的部分收取{{
rateFormat(productConfig.price_fee_rate)
}}的提价成本</div>
<div class="">设定范围<span class="text-orange-500">{{ <div class="">设定范围<span class="text-orange-500">{{
productConfig.price_range_min }}</span> - <span class="text-orange-500">{{ productConfig.price_range_min }}</span> - <span class="text-orange-500">{{
productConfig.price_range_max }}</span></div> productConfig.price_range_max }}</span></div>
@@ -60,24 +65,43 @@ watch(show, () => {
const costPrice = computed(() => { const costPrice = computed(() => {
if (!productConfig.value) return 0.00 if (!productConfig.value) return 0.00
// 平台定价成本
let platformPricing = 0 // 新代理系统:成本价 = 实际底价actual_base_price+ 提价成本
platformPricing += productConfig.value.cost_price const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
if (price.value > productConfig.value.p_pricing_standard) { const priceNum = Number(price.value) || 0;
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio 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) { const totalCost = actualBasePrice + priceCost;
if (price.value > productConfig.value.a_pricing_end) {
platformPricing += (productConfig.value.a_pricing_end - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
} else {
platformPricing += (price.value - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
}
}
}
return safeTruncate(platformPricing) return safeTruncate(totalCost);
})
const rateFormat = (rate) => {
return rate * 100 + '%';
}
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(() => { const promotionRevenue = computed(() => {

View File

@@ -1,4 +1,51 @@
<template> <template>
<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"
@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>
</template>
<template v-else>
<van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }"> <van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }">
<div class="qrcode-popup-container"> <div class="qrcode-popup-container">
<div class="qrcode-content"> <div class="qrcode-content">
@@ -18,57 +65,35 @@
<van-divider class="my-2 sm:my-3">分享到好友</van-divider> <van-divider class="my-2 sm:my-3">分享到好友</van-divider>
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4"> <div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
<!-- 微信环境显示分享保存和复制按钮 -->
<template v-if="isWeChat"> <template v-if="isWeChat">
<!-- <div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToFriend"> <div class="flex flex-col items-center justify-center cursor-pointer"
<img src="@/assets/images/icon_share_friends.svg" @click="savePosterForWeChat">
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" <img src="@/assets/images/icon_share_img.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" /> 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 class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
保存图片
</div>
</div> </div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl"> <div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.svg" <img src="@/assets/images/icon_share_url.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" /> 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 class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
复制链接
</div>
</div> </div>
</template> </template>
<!-- 非微信环境显示保存和复制按钮 -->
<template v-else> <template v-else>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster"> <div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
<img src="@/assets/images/icon_share_img.svg" <img src="@/assets/images/icon_share_img.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" /> 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 class="text-center mt-1 text-gray-600 text-xs">保存图片</div>
保存图片
</div>
</div> </div>
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl"> <div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
<img src="@/assets/images/icon_share_url.svg" <img src="@/assets/images/icon_share_url.svg"
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" /> 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 class="text-center mt-1 text-gray-600 text-xs">复制链接</div>
复制链接
</div>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
</van-popup> </van-popup>
</template>
<!-- 图片保存指引遮罩层 --> <!-- 图片保存指引遮罩层 -->
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle" <ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
@@ -85,14 +110,30 @@ import ImageSaveGuide from "./ImageSaveGuide.vue";
const props = defineProps({ const props = defineProps({
linkIdentifier: { linkIdentifier: {
type: String, type: String,
required: true, required: false, // 推广链接模式下需要
},
fullLink: {
type: String,
required: false, // 完整的推广链接(后端返回)
},
inviteLink: {
type: String,
required: false, // 邀请链接模式下需要
},
qrCodeUrl: {
type: String,
required: false, // 邀请链接模式下提供,直接使用后端返回的二维码
}, },
mode: { mode: {
type: String, type: String,
default: "promote", // 例如 "promote" | "invitation" 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 posterCanvasRefs = ref([]); // 用于绘制海报的canvas数组
const currentIndex = ref(0); // 当前显示的海报索引 const currentIndex = ref(0); // 当前显示的海报索引
const postersGenerated = ref([]); // 标记海报是否已经生成过将在onMounted中初始化 const postersGenerated = ref([]); // 标记海报是否已经生成过将在onMounted中初始化
@@ -111,10 +152,17 @@ const showImageGuide = ref(false);
const currentImageUrl = ref(''); const currentImageUrl = ref('');
const imageGuideTitle = ref(''); const imageGuideTitle = ref('');
const url = computed(() => { const url = computed(() => {
const baseUrl = window.location.origin; // 获取当前站点的域名 if (mode.value === "invitation" && inviteLink.value) {
return mode.value === "promote" // 邀请模式:使用完整的邀请链接(已包含域名)
? `${baseUrl}/agent/promotionInquire/` // 使用动态的域名 return inviteLink.value;
: `${baseUrl}/agent/invitationAgentApply/`; }
// 推广链接模式:使用后端返回的完整短链
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(); posterImages.value = await loadPosterImages();
// 根据加载的图片数量初始化postersGenerated数组 // 根据加载的图片数量初始化postersGenerated数组
postersGenerated.value = Array(posterImages.value.length).fill(false); postersGenerated.value = Array(posterImages.value.length).fill(false);
if (asPage.value && url.value && !postersGenerated.value[0]) {
generatePoster(0);
}
}); });
// 生成海报并合成二维码 // 生成海报并合成二维码
@@ -225,19 +276,11 @@ const generatePoster = async (index) => {
// 2. 绘制海报图片 // 2. 绘制海报图片
ctx.drawImage(posterImg, 0, 0); ctx.drawImage(posterImg, 0, 0);
// 3. 生成二维码 // 3. 生成或加载二维码
QRCode.toDataURL( const loadQRCode = (qrCodeDataUrl) => {
generalUrl(),
{ width: 150, margin: 0 },
(err, qrCodeUrl) => {
if (err) {
console.error(err);
return;
}
// 4. 加载二维码图片 // 4. 加载二维码图片
const qrCodeImg = new Image(); const qrCodeImg = new Image();
qrCodeImg.src = qrCodeUrl; qrCodeImg.src = qrCodeDataUrl;
qrCodeImg.onload = () => { qrCodeImg.onload = () => {
// 获取当前海报的二维码位置配置 // 获取当前海报的二维码位置配置
const positions = qrCodePositions.value[mode.value]; const positions = qrCodePositions.value[mode.value];
@@ -261,15 +304,39 @@ const generatePoster = async (index) => {
// 标记海报已生成 // 标记海报已生成
postersGenerated.value[index] = true; postersGenerated.value[index] = true;
}; };
};
// 生成二维码
const generateQRCode = () => {
QRCode.toDataURL(
generalUrl(),
{ width: 150, margin: 0 },
(err, qrCodeDataUrl) => {
if (err) {
console.error(err);
return;
}
loadQRCode(qrCodeDataUrl);
} }
); );
}; };
// 邀请模式:直接在前端生成二维码(使用 inviteLink
// 推广模式:也在前端生成二维码(使用 linkIdentifier 构建的 URL
generateQRCode();
};
}; };
// 监听 show 变化show 为 true 时生成海报 // 监听 show 变化show 为 true 时生成海报
watch(show, (newVal) => { watch(show, (newVal) => {
if (newVal && !postersGenerated.value[currentIndex.value]) { if (!asPage.value && newVal && !postersGenerated.value[currentIndex.value]) {
generatePoster(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, link: shareUrl,
imgUrl: "https://www.quannengcha.com/logo.png" imgUrl: "https://www.onecha.cn/logo.png"
}; };
configWeixinShare(shareConfig); configWeixinShare(shareConfig);
@@ -314,7 +381,7 @@ const shareToTimeline = () => {
? "扫码查看一查查推广信息" ? "扫码查看一查查推广信息"
: "扫码申请一查查代理权限", : "扫码申请一查查代理权限",
link: shareUrl, link: shareUrl,
imgUrl: "https://www.quannengcha.com/logo.png" imgUrl: "https://www.onecha.cn/logo.png"
}; };
configWeixinShare(shareConfig); configWeixinShare(shareConfig);
@@ -484,7 +551,10 @@ const tryShareAPI = async (dataURL) => {
}; };
const generalUrl = () => { const generalUrl = () => {
return url.value + encodeURIComponent(linkIdentifier.value); // 直接使用 url computed 属性,它已经处理了所有情况
// 推广模式:优先使用 fullLink否则使用 linkIdentifier 构建
// 邀请模式:优先使用 inviteLink否则使用 linkIdentifier 构建
return url.value;
}; };
const copyUrl = () => { const copyUrl = () => {

View File

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

View File

@@ -68,7 +68,7 @@ const handleShare = async () => {
try { try {
// 根据实际使用的标识构建请求参数 // 根据实际使用的标识构建请求参数
const requestData = props.orderId const requestData = props.orderId
? { order_id: parseInt(props.orderId) } ? { order_id: props.orderId }
: { order_no: props.orderNo }; : { order_no: props.orderNo };
const { data, error } = await useApiFetch("/query/generate_share_link") 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 实例 // 创建全局的 fetch 实例
const useApiFetch = createFetch({ 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: { options: {
async beforeFetch({ url, options }) { async beforeFetch({ url, options }) {
showLoadingToast({ showLoadingToast({
@@ -28,13 +30,24 @@ const useApiFetch = createFetch({
if (isWechat) { if (isWechat) {
platform = "wxh5"; platform = "wxh5";
} }
options.headers['X-Platform'] = platform options.headers["X-Platform"] = platform;
if (token) { if (token) {
options.headers = { options.headers = {
...options.headers, ...options.headers,
Authorization: `${token}`, 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 }; return { url, options };
}, },
@@ -44,30 +57,35 @@ const useApiFetch = createFetch({
if (response.status === 401) { if (response.status === 401) {
// 清除本地存储的 token // 清除本地存储的 token
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem('refreshAfter') localStorage.removeItem("refreshAfter");
localStorage.removeItem('accessExpire') localStorage.removeItem("accessExpire");
// 跳转到登录页 // 跳转到登录页
router.replace("/login"); router.replace("/login");
} }
if (data.code !== 200) { if (data.code !== 200) {
if (data.code === 100009) { if (data.code === 100009 || data.code === 100008) {
// 改进的存储管理 // 改进的存储管理
localStorage.removeItem('token') localStorage.removeItem("token");
localStorage.removeItem('refreshAfter') localStorage.removeItem("refreshAfter");
localStorage.removeItem('accessExpire') localStorage.removeItem("accessExpire");
localStorage.removeItem('userInfo') localStorage.removeItem("userInfo");
localStorage.removeItem('agentInfo') localStorage.removeItem("agentInfo");
localStorage.removeItem("tokenVersion");
// 重置状态 // 重置状态
const userStore = useUserStore(); const userStore = useUserStore();
const agentStore = useAgentStore(); const agentStore = useAgentStore();
userStore.resetUser() userStore.resetUser();
agentStore.resetAgent() agentStore.resetAgent();
location.reload() 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 }); showToast({ message: data.msg });
} }
} }
@@ -76,11 +94,11 @@ const useApiFetch = createFetch({
async onFetchError({ error, response }) { async onFetchError({ error, response }) {
console.log("error", error); console.log("error", error);
closeToast(); closeToast();
if (response.status === 401) { if (response && response.status === 401) {
// 清除本地存储的 token // 清除本地存储的 token
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem('refreshAfter') localStorage.removeItem("refreshAfter");
localStorage.removeItem('accessExpire') localStorage.removeItem("accessExpire");
// 跳转到登录页 // 跳转到登录页
router.replace("/login"); router.replace("/login");
} else { } else {

View File

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

View File

@@ -1,17 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from "vue-router";
import NProgress from 'nprogress' import NProgress from "nprogress";
import GlobalLayout from '@/layouts/GlobalLayout.vue' import GlobalLayout from "@/layouts/GlobalLayout.vue";
import HomeLayout from '@/layouts/HomeLayout.vue' import HomeLayout from "@/layouts/HomeLayout.vue";
import PageLayout from '@/layouts/PageLayout.vue' import PageLayout from "@/layouts/PageLayout.vue";
import index from '@/views/index.vue' import index from "@/views/index.vue";
import Promote from '@/views/Promote.vue' import Promote from "@/views/Promote.vue";
import PromotePage from '@/views/PromotePage.vue' import PromotePage from "@/views/PromotePage.vue";
import { useAgentStore } from '@/stores/agentStore' import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from '@/stores/userStore' import { useUserStore } from "@/stores/userStore";
import { useDialogStore } from '@/stores/dialogStore' import { useDialogStore } from "@/stores/dialogStore";
import { useEnv } from '@/composables/useEnv' import { useEnv } from "@/composables/useEnv";
import { storeToRefs } from 'pinia' import { storeToRefs } from "pinia";
import { useSEO } from '@/composables/useSEO' import { useSEO } from "@/composables/useSEO";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -27,262 +27,299 @@ const router = createRouter({
}, },
routes: [ routes: [
{ {
path: '/', path: "/",
component: GlobalLayout, // 使用 Layout 作为父组件 component: GlobalLayout, // 使用 Layout 作为父组件
children: [ children: [
{ {
path: '', path: "",
component: HomeLayout, // 使用 Layout 作为父组件 component: HomeLayout, // 使用 Layout 作为父组件
children: [ children: [
{ {
path: '', path: "",
name: 'promote', name: "promote",
component: PromotePage, component: PromotePage,
}, },
{ {
path: 'index', path: "index",
name: 'index', name: "index",
component: index, component: index,
}, },
{ {
path: '/agent', path: "/agent",
name: 'agent', name: "agent",
component: () => import('@/views/Agent.vue'), component: () => import("@/views/Agent.vue"),
meta: { title: "代理主页", requiresAuth: true },
}, },
{ {
path: 'me', path: "me",
name: 'me', name: "me",
component: () => import('@/views/Me.vue'), component: () => import("@/views/Me.vue"),
}, },
], ],
}, },
{ {
path: '', path: "",
component: PageLayout, component: PageLayout,
children: [ children: [
{ {
path: '/historyQuery', path: "/historyQuery",
name: 'history', name: "history",
component: () => import('@/views/HistoryQuery.vue'), 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'),
meta: { meta: {
title: '报告结果', requiresAuth: true, notNeedBindPhone: true title: "历史报告",
requiresAuth: true,
notNeedBindPhone: true,
}, },
}, },
{ {
path: '/example', path: "/help",
name: 'example', name: "help",
component: () => import('@/views/Example.vue'), component: () => import("@/views/Help.vue"),
meta: { title: '示例报告', notNeedBindPhone: true }, meta: { title: "帮助中心" },
}, },
{ {
path: '/vant-theme-test', path: "/help/detail",
name: 'vantThemeTest', name: "helpDetail",
component: () => import('@/views/VantThemeTest.vue'), component: () => import("@/views/HelpDetail.vue"),
meta: { title: 'Vant主题色测试' }, meta: { title: "帮助中心" },
}, },
{ {
path: '/privacyPolicy', path: "/help/guide",
name: 'privacyPolicy', name: "helpGuide",
component: () => import('@/views/PrivacyPolicy.vue'), component: () => import("@/views/HelpGuide.vue"),
meta: { title: '隐私政策' }, meta: { title: "引导指南" },
}, },
{ {
path: '/userAgreement', path: "/agent/system-guide",
name: 'userAgreement', name: "agentSystemGuide",
component: () => import('@/views/UserAgreement.vue'), component: () =>
meta: { title: '用户协议' }, import("@/views/AgentSystemGuide.vue"),
meta: { title: "代理系统指南" },
}, },
{ {
path: '/agentManageAgreement', path: "/withdraw",
name: 'agentManageAgreement', name: "withdraw",
component: () => import('@/views/AgentManageAgreement.vue'), component: () => import("@/views/Withdraw.vue"),
meta: { title: '代理管理协议' }, meta: { title: "提现", requiresAuth: true },
}, },
{ {
path: '/agentSerivceAgreement', path: "/service",
name: 'agentSerivceAgreement', name: "service",
component: () => import('@/views/AgentServiceAgreement.vue'), component: () => import("@/views/Service.vue"),
meta: { title: '信息技术服务合同' }, meta: { title: "客服" },
}, },
{ {
path: '/inquire/:feature', path: "/complaint",
name: 'inquire', name: "complaint",
component: () => import('@/views/Inquire.vue'), component: () => import("@/views/Complaint.vue"),
meta: { title: '查询报告' }, meta: { title: "投诉" },
}, },
{ {
path: '/authorization', path: "/report",
name: 'authorization', name: "report",
component: () => import('@/views/Authorization.vue'), component: () => import("@/views/Report.vue"),
meta: { title: '授权书' }, meta: {
title: "报告结果",
requiresAuth: true,
notNeedBindPhone: true,
},
}, },
{ {
path: '/payment/result', path: "/example",
name: 'paymentResult', name: "example",
component: () => import('@/views/PaymentResult.vue'), component: () => import("@/views/Example.vue"),
meta: { title: '支付结果', requiresAuth: true }, meta: { title: "示例报告", notNeedBindPhone: 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, component: PageLayout,
children: [ children: [
{ {
path: '/agent/promote', path: "/agent/promote",
name: 'agentPromote', name: "agentPromote",
component: Promote, component: Promote,
meta: { meta: {
title: '推广报告', title: "推广报告",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'promoteDetails', path: "/agent/promotion/query/list",
name: 'promoteDetails', name: "agentPromotionQueryList",
component: () => import('@/views/AgentPromoteDetails.vue'), component: () =>
import("@/views/AgentPromotionHistory.vue"),
meta: { meta: {
title: '直推报告收益明细', title: "推广查询记录",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'rewardsDetails', path: "promoteDetails",
name: 'rewardsDetails', name: "promoteDetails",
component: () => import('@/views/AgentRewardsDetails.vue'), component: () =>
import("@/views/AgentPromoteDetails.vue"),
meta: { meta: {
title: '代理奖励收益明细', title: "我的推广收益",
requiresAuth: true,
requiresAgent: true,
},
},
{
path: "rewardsDetails",
name: "rewardsDetails",
component: () =>
import("@/views/AgentRewardsDetails.vue"),
meta: {
title: "下级推广收益",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'invitation', path: "invitation",
name: 'invitation', name: "invitation",
component: () => import('@/views/Invitation.vue'), component: () =>
import("@/views/InvitationPage.vue"),
meta: { meta: {
title: '邀请下级', title: "邀请下级",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'agentVip', path: "withdraw",
name: 'agentVip', name: "withdraw",
component: () => import('@/views/AgentVip.vue'), component: () => import("@/views/Withdraw.vue"),
meta: { meta: {
title: '代理会员', title: "提现",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'vipApply', path: "withdrawDetails",
name: 'agentVipApply', name: "withdrawDetails",
component: () => import('@/views/AgentVipApply.vue'), component: () =>
import("@/views/WithdrawDetails.vue"),
meta: { meta: {
title: 'VIP代理申请', title: "提现记录",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'vipConfig', path: "teamList",
name: 'agentVipConfig', name: "teamList",
component: () => import('@/views/AgentVipConfig.vue'), component: () => import("@/views/TeamList.vue"),
meta: { meta: {
title: '代理会员报告配置', title: "我的团队",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'withdraw', path: "upgradeSubordinate",
name: 'withdraw', name: "upgradeSubordinate",
component: () => import('@/views/Withdraw.vue'), component: () =>
import("@/views/UpgradeSubordinate.vue"),
meta: { meta: {
title: '提现', title: "调整下级级别",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'withdrawDetails', path: "upgrade",
name: 'withdrawDetails', name: "agentUpgrade",
component: () => import('@/views/WithdrawDetails.vue'), component: () => import("@/views/AgentUpgrade.vue"),
meta: { meta: {
title: '提现记录', title: "升级代理",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'invitationAgentApply/self', path: "subordinateList",
name: 'invitationAgentApplySelf', name: "subordinateList",
component: () => import('@/views/InvitationAgentApply.vue'), component: () =>
meta: { title: '代理申请', requiresAuth: true }, import("@/views/SubordinateList.vue"),
},
{
path: 'subordinateList',
name: 'subordinateList',
component: () => import('@/views/SubordinateList.vue'),
meta: { meta: {
title: '我的下级', title: "我的下级",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
}, },
{ {
path: 'subordinateDetail/:id', path: "subordinateDetail/:id",
name: 'subordinateDetail', name: "subordinateDetail",
component: () => import('@/views/SubordinateDetail.vue'), component: () =>
import("@/views/SubordinateDetail.vue"),
meta: { meta: {
title: '下级贡献详情', title: "下级贡献详情",
requiresAuth: true, requiresAuth: true,
requiresAgent: true, requiresAgent: true,
}, },
@@ -290,37 +327,42 @@ const router = createRouter({
], ],
}, },
{ {
path: 'app', path: "app",
children: [ children: [
{ {
path: 'authorization', path: "authorization",
name: 'appAuthorization', name: "appAuthorization",
component: () => import('@/views/Authorization.vue'), component: () =>
meta: { title: '授权书' }, import("@/views/Authorization.vue"),
meta: { title: "授权书" },
}, },
{ {
path: 'privacyPolicy', path: "privacyPolicy",
name: 'appPrivacyPolicy', name: "appPrivacyPolicy",
component: () => import('@/views/PrivacyPolicy.vue'), component: () =>
meta: { title: '隐私政策' }, import("@/views/PrivacyPolicy.vue"),
meta: { title: "隐私政策" },
}, },
{ {
path: 'userAgreement', path: "userAgreement",
name: 'appUserAgreement', name: "appUserAgreement",
component: () => import('@/views/UserAgreement.vue'), component: () =>
meta: { title: '用户协议' }, import("@/views/UserAgreement.vue"),
meta: { title: "用户协议" },
}, },
{ {
path: 'agentManageAgreement', path: "agentManageAgreement",
name: 'appAgentManageAgreement', name: "appAgentManageAgreement",
component: () => import('@/views/AgentManageAgreement.vue'), component: () =>
meta: { title: '代理管理协议' }, import("@/views/AgentManageAgreement.vue"),
meta: { title: "代理管理协议" },
}, },
{ {
path: 'agentSerivceAgreement', path: "agentSerivceAgreement",
name: 'appAgentSerivceAgreement', name: "appAgentSerivceAgreement",
component: () => import('@/views/AgentServiceAgreement.vue'), component: () =>
meta: { title: '信息技术服务合同' }, import("@/views/AgentServiceAgreement.vue"),
meta: { title: "信息技术服务合同" },
}, },
], ],
}, },
@@ -328,45 +370,50 @@ const router = createRouter({
}, },
{ {
path: '/login', path: "/login",
name: 'login', name: "login",
component: () => import('@/views/Login.vue'), component: () => import("@/views/Login.vue"),
}, },
{ {
path: '/agent/promotionInquire/:linkIdentifier', path: "/register",
name: 'promotionInquire', name: "register",
component: () => import('@/views/PromotionInquire.vue'), component: () => import("@/views/Register.vue"),
},
{
path: "/agent/promotionInquire/:linkIdentifier",
name: "promotionInquire",
component: () => import("@/views/PromotionInquire.vue"),
meta: { notNeedBindPhone: true }, meta: { notNeedBindPhone: true },
}, },
{ {
path: '/agent/invitationAgentApply/:linkIdentifier', path: "/agent/invitationAgentApply/:linkIdentifier",
name: 'invitationAgentApply', name: "invitationAgentApply",
component: () => import('@/views/InvitationAgentApply.vue'), component: () => import("@/views/InvitationAgentApply.vue"),
meta: { title: '代理申请' }, meta: { title: "代理申请" },
}, },
{ {
path: '/report/share/:linkIdentifier', path: "/report/share/:linkIdentifier",
name: 'reportShare', name: "reportShare",
component: () => import('@/views/ReportShare.vue'), component: () => import("@/views/ReportShare.vue"),
}, },
{ {
path: '/:pathMatch(.*)*', path: "/:pathMatch(.*)*",
name: 'NotFound', name: "NotFound",
component: () => import('@/views/NotFound.vue'), component: () => import("@/views/NotFound.vue"),
}, },
], ],
}) });
NProgress.configure({ NProgress.configure({
easing: 'ease', // 动画方式 easing: "ease", // 动画方式
speed: 500, // 递增进度条的速度(毫秒) speed: 500, // 递增进度条的速度(毫秒)
showSpinner: false, // 是否显示加载的圆圈 showSpinner: false, // 是否显示加载的圆圈
trickleSpeed: 200, // 自动递增间隔 trickleSpeed: 200, // 自动递增间隔
minimum: 0.3, // 初始化最小百分比 minimum: 0.3, // 初始化最小百分比
}) });
// 路由导航守卫 // 路由导航守卫
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); // 启动进度条 NProgress.start();
const isAuthenticated = localStorage.getItem("token"); const isAuthenticated = localStorage.getItem("token");
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();
@@ -375,93 +422,89 @@ router.beforeEach(async (to, from, next) => {
const { isWeChat } = useEnv(); const { isWeChat } = useEnv();
const { isAgent, isLoaded } = storeToRefs(agentStore); const { isAgent, isLoaded } = storeToRefs(agentStore);
const { mobile, isLoggedIn } = storeToRefs(userStore); const { mobile, isLoggedIn } = storeToRefs(userStore);
const { isWeixinAuthing, weixinAuthComplete } = storeToRefs(authStore); // 检查 token 是否过期
const accessExpire = localStorage.getItem("accessExpire");
// 微信环境下,如果正在进行授权,等待授权完成 const now = Date.now();
if (isWeChat.value && isWeixinAuthing.value && !weixinAuthComplete.value) { let isTokenExpired = false;
// 等待授权完成,使用响应式监听 if (accessExpire) {
await new Promise((resolve) => { isTokenExpired = now > parseInt(accessExpire) * 1000;
const stopWatcher = watch(
[isWeixinAuthing, weixinAuthComplete],
([authing, complete]) => {
if (!authing || complete) {
stopWatcher();
resolve();
} }
}, // ============================================================
{ immediate: true } // 场景 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 });
if (to.meta.requiresAuth && !isAuthenticated) {
if (isWeChat.value) {
// 微信环境下,如果授权失败或超时,重定向到首页
if (!weixinAuthComplete.value) {
next("/");
location.reload();
} else {
// 授权完成但仍无token可能是授权失败
next("/");
location.reload();
}
} else {
next("/login");
}
return; return;
} }
// 已登录状态下的处理 // ============================================================
if (isAuthenticated) { // 场景 3: 已登录状态下的处理
// ============================================================
if (isAuthenticated && !isTokenExpired) {
// 确保用户信息已加载 // 确保用户信息已加载
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
try {
await userStore.fetchUserInfo(); await userStore.fetchUserInfo();
} catch (err) {
console.error("Error loading user info:", err);
}
} }
// 检查手机号绑定状态 // 检查代理权限(仅在 requiresAgent 为 true 时)
// 只有在未绑定手机号且目标路由需要登录并且没有设置notNeedBindPhone时才弹出绑定手机号弹窗
if (
!mobile.value &&
to.meta.requiresAuth &&
!to.meta.notNeedBindPhone
) {
dialogStore.openBindPhone();
next(false);
return;
}
// 检查代理权限
if (to.meta.requiresAgent) { if (to.meta.requiresAgent) {
if (!isLoaded.value) { if (!mobile.value) {
await agentStore.fetchAgentStatus(); 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) { if (!isAgent.value) {
next("/agent/invitationAgentApply/self"); next("/register");
return; return;
} }
} }
} }
// ============================================================
// 其他情况正常通过 // 其他情况正常通过
// ============================================================
next(); next();
}) });
router.afterEach((to) => { router.afterEach((to) => {
NProgress.done() // 结束进度条 NProgress.done(); // 结束进度条
// SEO优化更新页面标题和meta信息 // SEO优化更新页面标题和meta信息
const { updateSEO } = useSEO() const { updateSEO } = useSEO();
// 根据路由meta信息更新SEO // 根据路由meta信息更新SEO
if (to.meta.title) { if (to.meta.title) {
const seoConfig = { const seoConfig = {
title: `${to.meta.title} - 一查查`, title: `${to.meta.title} - 一查查`,
description: `一查查${to.meta.title}页面,提供专业的大数据风险管控服务。`, description: `一查查${to.meta.title}页面,提供专业的大数据风险管控服务。`,
url: `https://www.zhinengcha.cn${to.path}` url: `https://www.zhinengcha.cn${to.path}`,
};
updateSEO(seoConfig);
} }
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,30 +1,60 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getAgentInfo } from '@/api/agent'
export const useAgentStore = defineStore('agent', { export const useAgentStore = defineStore('agent', {
state: () => ({ state: () => ({
isLoaded: false, isLoaded: false,
level: '', level: 0, // 1=普通2=黄金3=钻石
status: 3, // 0=待审核1=审核通过2=审核未通过3=未申请 levelName: '', // 等级名称
isAgent: false, isAgent: false,
ancestorID: null,
agentID: null, agentID: null,
agentCode: 0,
mobile: '', mobile: '',
ExpiryTime: '', region: '',
wechatId: '',
teamLeaderId: null,
isRealName: false, 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: { actions: {
async fetchAgentStatus() { async fetchAgentStatus() {
const { data, error } = await useApiFetch('/agent/info').get().json() const { data, error } = await getAgentInfo()
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
this.level = data.value.data.level const agentData = data.value.data
this.isAgent = data.value.data.is_agent // 判断是否是代理 // 如果 agent_id 为 0说明不是代理
this.status = data.value.data.status // 获取代理状态 0=待审核1=审核通过2=审核未通过3=未申请 if (agentData.agent_id === 0) {
this.agentID = data.value.data.agent_id this.resetAgent()
this.mobile = data.value.data.mobile } else {
this.ExpiryTime = data.value.data.expiry_time this.level = agentData.level || 0
this.isRealName = data.value.data.is_real_name 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
localStorage.setItem( localStorage.setItem(
@@ -32,18 +62,38 @@ export const useAgentStore = defineStore('agent', {
JSON.stringify({ JSON.stringify({
isAgent: this.isAgent, isAgent: this.isAgent,
level: this.level, level: this.level,
status: this.status, levelName: this.levelName,
agentID: this.agentID, agentID: this.agentID,
agentCode: this.agentCode,
mobile: this.mobile, mobile: this.mobile,
ExpiryTime: this.ExpiryTime, region: this.region,
wechatId: this.wechatId,
teamLeaderId: this.teamLeaderId,
isRealName: this.isRealName, isRealName: this.isRealName,
}) })
) )
}
} else { } else {
// 检查是否是临时用户需要绑定手机号的错误100010
if (data.value.code === 100010) {
// 临时用户,不重置状态,静默处理
console.log('临时用户需要绑定手机号,跳过代理状态检查')
} else {
// 如果不是代理或获取失败,重置状态
this.resetAgent()
console.log('Error fetching agent info', data.value) console.log('Error fetching agent info', data.value)
} }
} }
} else {
// 检查错误码
if (error.value && error.value.code === 100010) {
// 临时用户需要绑定手机号,静默处理
console.log('临时用户需要绑定手机号,跳过代理状态检查')
} else {
// 请求失败或未登录,重置状态
this.resetAgent()
}
}
this.isLoaded = true this.isLoaded = true
}, },
@@ -51,25 +101,30 @@ export const useAgentStore = defineStore('agent', {
updateAgentInfo(agentInfo) { updateAgentInfo(agentInfo) {
if (agentInfo) { if (agentInfo) {
this.isAgent = agentInfo.isAgent || false this.isAgent = agentInfo.isAgent || false
this.level = agentInfo.level || '' this.level = agentInfo.level || 0
this.status = agentInfo.status || 3 this.levelName = agentInfo.levelName || ''
this.agentID = agentInfo.agentID || null this.agentID = agentInfo.agentID || null
this.mobile = agentInfo.mobile || '' 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.isRealName = agentInfo.isRealName || false
this.isLoaded = true
} }
}, },
// 重置代理信息 // 重置代理信息
resetAgent() { resetAgent() {
this.isLoaded = false this.isLoaded = false
this.level = '' this.level = 0
this.status = 3 this.levelName = ''
this.isAgent = false this.isAgent = false
this.ancestorID = null
this.agentID = null this.agentID = null
this.agentCode = 0
this.mobile = '' this.mobile = ''
this.region = ''
this.wechatId = ''
this.teamLeaderId = null
this.isRealName = false 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,39 +6,63 @@ export const useAuthStore = defineStore("auth", {
isWeixinAuthing: false, // 是否正在进行微信授权 isWeixinAuthing: false, // 是否正在进行微信授权
weixinAuthComplete: false, // 微信授权是否完成 weixinAuthComplete: false, // 微信授权是否完成
pendingRoute: null, // 等待授权完成后跳转的路由 pendingRoute: null, // 等待授权完成后跳转的路由
authStartTime: 0, // 授权开始时间,用于防止超时重复授权
}), }),
actions: { actions: {
// 开始微信授权 // 开始微信授权
startWeixinAuth(targetRoute = null) { startWeixinAuth(targetRoute = null) {
// 如果已经在授权过程中,不再重复启动
if (this.isWeixinAuthing) {
console.warn("WeChat auth already in progress");
return;
}
this.isWeixinAuthing = true; this.isWeixinAuthing = true;
this.weixinAuthComplete = false; this.weixinAuthComplete = false;
this.pendingRoute = targetRoute; this.pendingRoute = targetRoute;
this.authStartTime = Date.now();
// 保存到localStorage防止页面刷新后状态丢失 // 保存到localStorage防止页面刷新后状态丢失
localStorage.setItem("weixinAuthing", "true"); localStorage.setItem("weixinAuthing", "true");
localStorage.setItem(
"authStartTime",
this.authStartTime.toString()
);
if (targetRoute) { if (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( localStorage.setItem(
"pendingRoute", "pendingRoute",
JSON.stringify(targetRoute) JSON.stringify(sanitized)
); );
} }
}
}, },
// 微信授权完成 // 微信授权完成
completeWeixinAuth() { completeWeixinAuth() {
this.isWeixinAuthing = false; this.isWeixinAuthing = false;
this.weixinAuthComplete = true; this.weixinAuthComplete = true;
this.authStartTime = 0;
// 清除localStorage中的授权状态 // 清除localStorage中的授权状态
localStorage.removeItem("weixinAuthing"); localStorage.removeItem("weixinAuthing");
localStorage.removeItem("pendingRoute"); localStorage.removeItem("authStartTime");
},
// 清除待处理路由
clearPendingRoute() {
this.pendingRoute = null;
localStorage.removeItem("pendingRoute");
}, },
// 重置授权状态 // 重置授权状态
@@ -46,22 +70,47 @@ export const useAuthStore = defineStore("auth", {
this.isWeixinAuthing = false; this.isWeixinAuthing = false;
this.weixinAuthComplete = false; this.weixinAuthComplete = false;
this.pendingRoute = null; this.pendingRoute = null;
this.authStartTime = 0;
localStorage.removeItem("weixinAuthing"); localStorage.removeItem("weixinAuthing");
localStorage.removeItem("pendingRoute"); localStorage.removeItem("pendingRoute");
localStorage.removeItem("authStartTime");
},
// 检查授权是否超时超过30秒视为超时
isAuthTimeout() {
if (!this.authStartTime) return false;
const elapsed = Date.now() - this.authStartTime;
return elapsed > 30000; // 30秒超时
}, },
// 从localStorage恢复状态页面刷新后调用 // 从localStorage恢复状态页面刷新后调用
restoreFromStorage() { restoreFromStorage() {
const isAuthing = localStorage.getItem("weixinAuthing") === "true"; const isAuthing = localStorage.getItem("weixinAuthing") === "true";
const pendingRouteStr = localStorage.getItem("pendingRoute"); const pendingRouteStr = localStorage.getItem("pendingRoute");
const authStartTime = localStorage.getItem("authStartTime");
if (isAuthing) { if (isAuthing) {
console.log("🔄 Restoring WeChat auth state from storage");
this.isWeixinAuthing = true; this.isWeixinAuthing = true;
this.weixinAuthComplete = false; this.weixinAuthComplete = false;
this.authStartTime = authStartTime
? parseInt(authStartTime)
: 0;
// 检查是否超时,如果超时则重置
if (this.isAuthTimeout()) {
console.warn("WeChat auth timeout, resetting state");
this.resetAuthState();
return;
}
if (pendingRouteStr) { if (pendingRouteStr) {
try { try {
this.pendingRoute = JSON.parse(pendingRouteStr); this.pendingRoute = JSON.parse(pendingRouteStr);
console.log(
"✅ Restored pendingRoute from storage:",
this.pendingRoute
);
} catch (e) { } catch (e) {
console.error("Failed to parse pending route:", e); console.error("Failed to parse pending route:", e);
this.pendingRoute = null; 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' import { ref } from 'vue'
export const useDialogStore = defineStore('dialog', () => { export const useDialogStore = defineStore('dialog', () => {
const showBindPhone = ref(false) const showBindPhone = ref(false) // 推广页面专用的绑定手机号(不要求邀请码)
const showRegisterAgent = ref(false) // 注册成为代理(带邀请码)
const showRealNameAuth = ref(false) const showRealNameAuth = ref(false)
function openBindPhone() { function openBindPhone() {
@@ -13,6 +14,14 @@ export const useDialogStore = defineStore('dialog', () => {
showBindPhone.value = false showBindPhone.value = false
} }
function openRegisterAgent() {
showRegisterAgent.value = true
}
function closeRegisterAgent() {
showRegisterAgent.value = false
}
function openRealNameAuth() { function openRealNameAuth() {
showRealNameAuth.value = true showRealNameAuth.value = true
} }
@@ -25,6 +34,9 @@ export const useDialogStore = defineStore('dialog', () => {
showBindPhone, showBindPhone,
openBindPhone, openBindPhone,
closeBindPhone, closeBindPhone,
showRegisterAgent,
openRegisterAgent,
closeRegisterAgent,
showRealNameAuth, showRealNameAuth,
openRealNameAuth, openRealNameAuth,
closeRealNameAuth, closeRealNameAuth,

View File

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

View File

@@ -0,0 +1,240 @@
<template>
<div class="card application-count-section">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<div class="mt-4">
<!-- 申请次数时间分布图表 -->
<div class="mb-6">
<LTitle title="申请次数时间分布" />
<div class="h-64">
<v-chart class="chart-container" :option="chartOption" autoresize />
</div>
</div>
<!-- 特殊时段申请次数周末/夜间去掉 Tab直接展示一张柱状图 -->
<div class="mb-2">
<LTitle title="特殊时段申请次数(周末 / 夜间)" />
<div class="h-64">
<v-chart class="chart-container" :option="specialChartOption" autoresize />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components'
import LTitle from '@/components/LTitle.vue'
import { getApplicationCounts, getSpecialPeriodCounts, PERIOD_MAP } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
BarChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
}
})
const periodKeys = ['d7', 'd15', 'm1', 'm3', 'm6', 'm12']
const labels = periodKeys.map(key => PERIOD_MAP[key].label)
// 总申请次数图表配置
const chartOption = computed(() => {
const data = periodKeys.map(key => {
const counts = getApplicationCounts(props.data, key)
return counts.total
})
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params) {
let result = params[0].name + '<br/>'
params.forEach(item => {
result += `${item.seriesName}: ${item.value} 次<br/>`
})
return result
}
},
grid: {
left: '3%',
right: '4%',
bottom: 0,
top: 20,
containLabel: true
},
xAxis: {
type: 'category',
data: labels,
axisLabel: {
fontSize: 10,
color: '#6b7280',
rotate: 45
},
axisLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
color: '#6b7280',
formatter: '{value} 次'
},
splitLine: {
lineStyle: {
color: '#f3f4f6'
}
}
},
series: [
{
name: '申请次数',
type: 'bar',
data: data,
barWidth: '25%',
barMinHeight: 3,
itemStyle: {
color: '#2B79EE',
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
color: '#1e5bb8'
}
}
}
]
}
})
// 特殊时段(周末 / 夜间)图表配置
const specialChartOption = computed(() => {
const weekendData = periodKeys.map(key => {
const s = getSpecialPeriodCounts(props.data, key)
return s.weekend || 0
})
const nightData = periodKeys.map(key => {
const s = getSpecialPeriodCounts(props.data, key)
return s.night || 0
})
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params) {
let result = params[0].name + '<br/>'
params.forEach(item => {
result += `${item.seriesName}: ${item.value} 次<br/>`
})
return result
}
},
legend: {
data: ['周末申请次数', '夜间申请次数'],
top: '5%',
textStyle: {
fontSize: 12
}
},
grid: {
left: '3%',
right: '4%',
bottom: 0,
top: 30,
containLabel: true
},
xAxis: {
type: 'category',
data: labels,
axisLabel: {
fontSize: 10,
color: '#6b7280',
rotate: 45
},
axisLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
color: '#6b7280',
formatter: '{value} 次'
},
splitLine: {
lineStyle: {
color: '#f3f4f6'
}
}
},
series: [
{
name: '周末申请次数',
type: 'bar',
data: weekendData,
barWidth: '25%',
barMinHeight: 3,
itemStyle: {
color: '#2B79EE',
borderRadius: [4, 4, 0, 0]
}
},
{
name: '夜间申请次数',
type: 'bar',
data: nightData,
barWidth: '25%',
barMinHeight: 3,
itemStyle: {
color: '#F97316',
borderRadius: [4, 4, 0, 0]
}
}
]
}
})
</script>
<style lang="scss" scoped>
.card {
background: #ffffff;
}
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="card application-total-section">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<div class="flex items-center mb-4 p-4">
<span class="font-bold text-gray-800">申请总次数 (银行+非银) {{ totalCount }}</span>
</div>
<div class="mt-4">
<!-- Tab切换 -->
<div class="mb-6">
<LTitle title="申请总次数详情" />
<div class="bg-white px-4 py-2">
<van-tabs v-model:active="activeTab" color="var(--color-primary)">
<van-tab v-for="(period, index) in periods" :key="period.key" :title="period.label">
<div class="p-4">
<!-- 银行机构 -->
<BankInstitutionSection :data="data" :period="period.key" />
<!-- 非银机构 -->
<NBankInstitutionSection :data="data" :period="period.key" />
</div>
</van-tab>
</van-tabs>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import LTitle from '@/components/LTitle.vue'
import { getApplicationCounts, PERIOD_MAP } from '../utils/dataParser'
import BankInstitutionSection from './BankInstitutionSection.vue'
import NBankInstitutionSection from './NBankInstitutionSection.vue'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
}
})
const activeTab = ref(5) // 默认显示12个月
const periods = [
{ key: 'd7', label: '7天' },
{ key: 'd15', label: '15天' },
{ key: 'm1', label: '1个月' },
{ key: 'm3', label: '3个月' },
{ key: 'm6', label: '6个月' },
{ key: 'm12', label: '12个月' }
]
// 计算总申请次数12个月
const totalCount = computed(() => {
const counts = getApplicationCounts(props.data, 'm12')
return counts.total
})
</script>
<style lang="scss" scoped>
.card {
background: #ffffff;
}
.application-total-section :deep(.van-tabs) {
border: unset !important;
}
.application-total-section :deep(.van-tabs__wrap) {
height: 32px !important;
background-color: transparent !important;
padding: 0 !important;
border-bottom: 1px solid #DDDDDD !important;
}
.application-total-section :deep(.van-tabs__nav) {
background-color: transparent !important;
gap: 0 !important;
height: 32px !important;
border: unset !important;
}
.application-total-section :deep(.van-tabs__nav--card) {
border: unset !important;
}
.application-total-section :deep(.van-tab) {
color: #999999 !important;
font-size: 14px !important;
font-weight: 400 !important;
border-right: unset !important;
background-color: transparent !important;
border-radius: unset !important;
max-width: 80px !important;
}
.application-total-section :deep(.van-tab--card) {
color: #999999 !important;
border-right: unset !important;
background-color: transparent !important;
border-radius: unset !important;
}
.application-total-section :deep(.van-tab--active) {
color: var(--van-theme-primary) !important;
background-color: unset !important;
}
.application-total-section :deep(.van-tabs__line) {
height: 4px !important;
border-radius: 1px !important;
background-color: var(--van-theme-primary) !important;
width: 20px;
border-radius: 14px;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div class="mb-6">
<LTitle title="银行机构申请分布" />
<div class="mt-4">
<!-- 饼图宽度占满 -->
<div class="h-64 mb-4">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表在图表下方展示所有项并带颜色标识 -->
<div class="space-y-2">
<div v-for="(item, index) in detailList" :key="index" class="flex justify-between items-center text-sm">
<div class="flex items-center">
<span class="w-2 h-2 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
<span class="text-gray-600">{{ item.label }}</span>
</div>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import LTitle from '@/components/LTitle.vue'
import { getBankApplicationDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 颜色映射表(与图表保持一致)
const COLORS = [
'#2B79EE',
'#61D2F4',
'#34D399',
'#FBBF24',
'#F97316',
'#EF4444',
'#A855F7',
'#6B7280',
]
// 获取银行机构申请详情
const bankDetails = computed(() => getBankApplicationDetails(props.data, props.period))
// 计算银行机构总次数
const bankTotal = computed(() => {
const details = bankDetails.value
return Object.values(details).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表(包含所有项,包含 0 次)
const detailList = computed(() => {
const details = bankDetails.value
const labels = FIELD_LABELS.bank
return Object.entries(details)
.map(([key, value], index) => ({
key,
label: labels[key] || key,
value: value || 0,
color: COLORS[index % COLORS.length],
}))
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (!list || list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}次 ({d}%)'
},
graphic: {
type: 'text',
left: 'center',
top: 'center',
style: {
text: '银行机构',
fill: '#111827',
fontSize: 14,
fontWeight: 'bold',
},
},
series: [
{
name: '申请次数',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
},
label: {
show: true,
fontSize: 14,
fontWeight: 'bold',
color: '#333'
}
},
data: list.map(item => ({
value: item.value,
name: item.label,
itemStyle: {
color: item.color
}
}))
}
]
}
})
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div class="mb-6">
<LTitle title="银行机构申请机构数分布" />
<div class="mt-4">
<!-- 饼图宽度占满 -->
<div class="h-64 mb-4">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表在图表下方展示所有项并带颜色标识 -->
<div class="space-y-2">
<div v-for="(item, index) in detailList" :key="index" class="flex justify-between items-center text-sm">
<div class="flex items-center">
<span class="w-2 h-2 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
<span class="text-gray-600">{{ item.label }}</span>
</div>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import LTitle from '@/components/LTitle.vue'
import { getBankOrgDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 颜色映射表(与图表保持一致)
const COLORS = [
'#2B79EE',
'#61D2F4',
'#34D399',
'#FBBF24',
'#F97316',
'#EF4444',
'#A855F7',
'#6B7280',
]
// 获取银行机构数详情
const bankOrgs = computed(() => getBankOrgDetails(props.data, props.period))
// 计算银行机构总数
const bankTotal = computed(() => {
const orgs = bankOrgs.value
return Object.values(orgs).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表(包含所有项,包含 0 家)
const detailList = computed(() => {
const orgs = bankOrgs.value
const labels = FIELD_LABELS.bank
return Object.entries(orgs)
.map(([key, value], index) => ({
key,
label: labels[key] || key,
value: value || 0,
color: COLORS[index % COLORS.length],
}))
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (!list || list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}家 ({d}%)'
},
graphic: {
type: 'text',
left: 'center',
top: 'center',
style: {
text: '银行机构',
fill: '#111827',
fontSize: 14,
fontWeight: 'bold',
},
},
series: [
{
name: '机构数',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
},
label: {
show: true,
fontSize: 14,
fontWeight: 'bold',
color: '#333'
}
},
data: list.map(item => ({
value: item.value,
name: item.label,
itemStyle: {
color: item.color
}
}))
}
]
}
})
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="card institution-total-section">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<div class="mt-4">
<!-- Tab切换 -->
<div class="mb-6">
<LTitle title="申请机构总数详情" />
<div class="bg-white px-4 py-2">
<van-tabs v-model:active="activeTab" color="var(--color-primary)">
<van-tab v-for="(period, index) in periods" :key="period.key" :title="period.label">
<div class="p-4">
<!-- 银行机构 -->
<BankOrgSection :data="data" :period="period.key" />
<!-- 非银机构 -->
<NBankOrgSection :data="data" :period="period.key" />
</div>
</van-tab>
</van-tabs>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import LTitle from '@/components/LTitle.vue'
import { getBankOrgDetails, getNBankOrgDetails } from '../utils/dataParser'
import BankOrgSection from './BankOrgSection.vue'
import NBankOrgSection from './NBankOrgSection.vue'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
}
})
const activeTab = ref(5) // 默认显示12个月
const periods = [
{ key: 'd7', label: '7天' },
{ key: 'd15', label: '15天' },
{ key: 'm1', label: '1个月' },
{ key: 'm3', label: '3个月' },
{ key: 'm6', label: '6个月' },
{ key: 'm12', label: '12个月' }
]
// 计算总机构数12个月
const totalCount = computed(() => {
const bankOrgs = getBankOrgDetails(props.data, 'm12')
const nbankOrgs = getNBankOrgDetails(props.data, 'm12')
const bankTotal = Object.values(bankOrgs).reduce((sum, val) => sum + (val || 0), 0)
const nbankTotal = Object.values(nbankOrgs).reduce((sum, val) => sum + (val || 0), 0)
return bankTotal + nbankTotal
})
</script>
<style lang="scss" scoped>
.card {
background: #ffffff;
}
.institution-total-section :deep(.van-tabs) {
border: unset !important;
}
.institution-total-section :deep(.van-tabs__wrap) {
height: 32px !important;
background-color: transparent !important;
padding: 0 !important;
border-bottom: 1px solid #DDDDDD !important;
}
.institution-total-section :deep(.van-tabs__nav) {
background-color: transparent !important;
gap: 0 !important;
height: 32px !important;
border: unset !important;
}
.institution-total-section :deep(.van-tabs__nav--card) {
border: unset !important;
}
.institution-total-section :deep(.van-tab) {
color: #999999 !important;
font-size: 14px !important;
font-weight: 400 !important;
border-right: unset !important;
background-color: transparent !important;
border-radius: unset !important;
max-width: 80px !important;
}
.institution-total-section :deep(.van-tab--card) {
color: #999999 !important;
border-right: unset !important;
background-color: transparent !important;
border-radius: unset !important;
}
.institution-total-section :deep(.van-tab--active) {
color: var(--van-theme-primary) !important;
background-color: unset !important;
}
.institution-total-section :deep(.van-tabs__line) {
height: 4px !important;
border-radius: 1px !important;
background-color: var(--van-theme-primary) !important;
width: 20px;
border-radius: 14px;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div class="mb-6">
<LTitle title="非银机构申请分布" />
<div class="mt-4">
<!-- 饼图宽度占满 -->
<div class="h-64 mb-4">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表在图表下方展示所有项并带颜色标识 -->
<div class="space-y-2">
<div v-for="(item, index) in detailList" :key="index" class="flex justify-between items-center text-sm">
<div class="flex items-center">
<span class="w-2 h-2 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
<span class="text-gray-600">{{ item.label }}</span>
</div>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import LTitle from '@/components/LTitle.vue'
import { getNBankApplicationDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 颜色映射表(与图表保持一致)
const COLORS = [
'#2B79EE',
'#61D2F4',
'#34D399',
'#FBBF24',
'#F97316',
'#EF4444',
'#A855F7',
'#6B7280',
]
// 获取非银机构申请详情
const nbankDetails = computed(() => getNBankApplicationDetails(props.data, props.period))
// 计算非银机构总次数
const nbankTotal = computed(() => {
const details = nbankDetails.value
return Object.values(details).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表(包含所有项,包含 0 次)
const detailList = computed(() => {
const details = nbankDetails.value
const labels = FIELD_LABELS.nbank
return Object.entries(details)
.map(([key, value], index) => ({
key,
label: labels[key] || key,
value: value || 0,
color: COLORS[index % COLORS.length],
}))
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (!list || list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}次 ({d}%)'
},
graphic: {
type: 'text',
left: 'center',
top: 'center',
style: {
text: '非银机构',
fill: '#111827',
fontSize: 14,
fontWeight: 'bold',
},
},
series: [
{
name: '申请次数',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
},
label: {
show: true,
fontSize: 14,
fontWeight: 'bold',
color: '#333'
}
},
data: list.map(item => ({
value: item.value,
name: item.label,
itemStyle: {
color: item.color
}
}))
}
]
}
})
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div class="mb-6">
<LTitle title="非银机构申请机构数分布" />
<div class="mt-4">
<!-- 饼图宽度占满 -->
<div class="h-64 mb-4">
<v-chart class="chart-container" :option="pieChartOption" autoresize />
</div>
<!-- 详细列表在图表下方展示所有项并带颜色标识 -->
<div class="space-y-2">
<div v-for="(item, index) in detailList" :key="index" class="flex justify-between items-center text-sm">
<div class="flex items-center">
<span class="w-2 h-2 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
<span class="text-gray-600">{{ item.label }}</span>
</div>
<span class="text-[#333333] font-bold">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components'
import LTitle from '@/components/LTitle.vue'
import { getNBankOrgDetails, FIELD_LABELS } from '../utils/dataParser'
// 注册ECharts组件
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
period: {
type: String,
required: true
}
})
// 颜色映射表(与图表保持一致)
const COLORS = [
'#2B79EE',
'#61D2F4',
'#34D399',
'#FBBF24',
'#F97316',
'#EF4444',
'#A855F7',
'#6B7280',
]
// 获取非银机构数详情
const nbankOrgs = computed(() => getNBankOrgDetails(props.data, props.period))
// 计算非银机构总数
const nbankTotal = computed(() => {
const orgs = nbankOrgs.value
return Object.values(orgs).reduce((sum, val) => sum + (val || 0), 0)
})
// 详细列表(包含所有项,包含 0 家)
const detailList = computed(() => {
const orgs = nbankOrgs.value
const labels = FIELD_LABELS.nbank
return Object.entries(orgs)
.map(([key, value], index) => ({
key,
label: labels[key] || key,
value: value || 0,
color: COLORS[index % COLORS.length],
}))
})
// 饼图配置
const pieChartOption = computed(() => {
const list = detailList.value
if (!list || list.length === 0) {
return {
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
}
}
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}家 ({d}%)'
},
graphic: {
type: 'text',
left: 'center',
top: 'center',
style: {
text: '非银机构',
fill: '#111827',
fontSize: 14,
fontWeight: 'bold',
},
},
series: [
{
name: '机构数',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
},
label: {
show: true,
fontSize: 14,
fontWeight: 'bold',
color: '#333'
}
},
data: list.map(item => ({
value: item.value,
name: item.label,
itemStyle: {
color: item.color
}
}))
}
]
}
})
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<div class="card">
<div class="rounded-lg border border-gray-200 pb-2 mb-4">
<div class="mt-4">
<LTitle title="产品类型申请分布近12个月" />
<div class="h-64 mb-4">
<v-chart class="chart-container" :option="chartOption" autoresize />
</div>
<!-- 详细列表与图表颜色一致包含 0 次的类型 -->
<div class="space-y-2 px-4 pb-4">
<div v-for="item in detailList" :key="item.key" class="flex items-center justify-between text-sm">
<div class="flex items-center">
<span class="w-2 h-2 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
<span class="text-gray-600">{{ item.label }}</span>
</div>
<span class="text-[#111827] font-bold">
{{ item.value }}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
GridComponent
} from 'echarts/components'
import LTitle from '@/components/LTitle.vue'
import { getValue, FIELD_LABELS } from '../utils/dataParser'
// 注册 ECharts 组件
use([
CanvasRenderer,
BarChart,
TitleComponent,
TooltipComponent,
GridComponent
])
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
}
})
// 近12个月的前缀
const PREFIX = 'als_m12'
// 关注的产品类型 key字段中的 id_xxx_allnum
const TYPE_KEYS = ['pdl', 'caon', 'rel', 'caoff', 'cooff', 'af', 'coon', 'oth']
// 颜色映射,与其他组件保持风格一致
const COLORS = [
'#2B79EE',
'#60A5FA',
'#34D399',
'#FBBF24',
'#F97316',
'#EF4444',
'#A855F7',
'#6B7280'
]
// 详细列表(每个产品类型的次数和颜色)
const detailList = computed(() => {
const v = props.data || {}
const labels = FIELD_LABELS.bank || {}
return TYPE_KEYS.map((key, index) => {
const field = `${PREFIX}_id_${key}_allnum`
const value = getValue(v[field]) || 0
return {
key,
label: labels[key] || key,
value,
color: COLORS[index % COLORS.length]
}
})
})
// 产品类型分布柱状图
const chartOption = computed(() => {
const list = detailList.value
const categories = list.map(item => item.label)
const values = list.map(item => item.value)
const colors = list.map(item => item.color)
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: params => {
const p = params[0]
return `${p.name}<br/>申请次数:${p.value}`
}
},
grid: {
left: 0,
right: 0,
bottom: 0,
top: 20,
containLabel: true
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
fontSize: 10,
color: '#6b7280',
rotate: 30
},
axisLine: {
lineStyle: {
color: '#e5e7eb'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 11,
color: '#6b7280',
formatter: '{value} 次'
},
splitLine: {
lineStyle: {
color: '#f3f4f6'
}
}
},
series: [
{
name: '申请次数',
type: 'bar',
data: values,
barWidth: '35%',
barMinHeight: 3,
itemStyle: {
color: params => colors[params.dataIndex],
borderRadius: [4, 4, 0, 0]
}
}
]
}
})
</script>
<style lang="scss" scoped>
.card {
background: #ffffff;
}
.chart-container {
width: 100%;
height: 100%;
}
</style>
{
"cells": [],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,79 @@
<template>
<div class="card">
<div class="rounded-lg border border-gray-200 p-4 space-y-3">
<LTitle title="近期集中申请提示" />
<div v-if="hasAnyData" class="space-y-2 text-sm text-gray-700">
<div class="flex justify-between">
<span>最近在银行连续申请次数</span>
<span class="font-bold text-[#111827]">{{ bankCons }} </span>
</div>
<div class="flex justify-between">
<span>最近在银行连续申请天数</span>
<span class="font-bold text-[#111827]">{{ bankDays }} </span>
</div>
<div class="flex justify-between">
<span>最近在非银连续申请次数</span>
<span class="font-bold text-[#111827]">{{ nbankCons }} </span>
</div>
<div class="flex justify-between">
<span>最近在非银连续申请天数</span>
<span class="font-bold text-[#111827]">{{ nbankDays }} </span>
</div>
<div class="flex justify-between">
<span>距最近一次在银行机构申请</span>
<span class="font-bold text-[#111827]">{{ bankGap }} </span>
</div>
<div class="flex justify-between">
<span>距最近一次在非银机构申请</span>
<span class="font-bold text-[#111827]">{{ nbankGap }} </span>
</div>
</div>
<div v-else class="text-sm text-gray-400">
暂未查询到明显的近期集中申请行为
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import LTitle from '@/components/LTitle.vue'
import { getValue } from '../utils/dataParser'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({}),
},
})
const bankCons = computed(() => getValue(props.data?.als_lst_id_bank_consnum))
const bankDays = computed(() => getValue(props.data?.als_lst_id_bank_csinteday))
const nbankCons = computed(() => getValue(props.data?.als_lst_id_nbank_consnum))
const nbankDays = computed(() => getValue(props.data?.als_lst_id_nbank_csinteday))
const bankGap = computed(() => getValue(props.data?.als_lst_id_bank_inteday))
const nbankGap = computed(() => getValue(props.data?.als_lst_id_nbank_inteday))
const hasAnyData = computed(() => {
return (
(bankCons.value || 0) > 0 ||
(bankDays.value || 0) > 0 ||
(nbankCons.value || 0) > 0 ||
(nbankDays.value || 0) > 0 ||
(bankGap.value || 0) > 0 ||
(nbankGap.value || 0) > 0
)
})
</script>
<style scoped>
.card {
background: #ffffff;
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<div class="card rounded-lg border border-gray-200 pb-2 mb-2">
<div class="flex items-center mb-4 p-4">
<span class="font-bold text-gray-800">借贷申请统计概览</span>
</div>
<div class="grid grid-cols-2 gap-4 px-4 pb-4">
<div v-for="item in items" :key="item.label"
class="bg-blue-50 rounded-lg p-3 text-center border border-[#2B79EE1A]">
<div class="text-xl font-bold text-[#111827]">
{{ item.value }}
<span class="text-xs text-gray-500 ml-1">{{ item.unit }}</span>
</div>
<div class="text-xs text-gray-600 mt-1">
{{ item.label }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { getApplicationCounts, getBankOrgDetails, getNBankOrgDetails } from '../utils/dataParser'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({}),
},
})
const items = computed(() => {
const v = props.data || {}
// 近12个月申请次数总/银行/非银)
const m12 = getApplicationCounts(v, 'm12')
// 近12个月申请机构总数银行+非银)
const bankOrgs = getBankOrgDetails(v, 'm12')
const nbankOrgs = getNBankOrgDetails(v, 'm12')
const bankOrgTotal = Object.values(bankOrgs || {}).reduce((sum, val) => sum + (val || 0), 0)
const nbankOrgTotal = Object.values(nbankOrgs || {}).reduce((sum, val) => sum + (val || 0), 0)
const orgTotal = bankOrgTotal + nbankOrgTotal
// 近12个月周末 / 夜间申请次数(银行+非银)
const weekendTotal =
Number(v.als_m12_id_bank_week_allnum || 0) + Number(v.als_m12_id_nbank_week_allnum || 0)
const nightTotal =
Number(v.als_m12_id_bank_night_allnum || 0) + Number(v.als_m12_id_nbank_night_allnum || 0)
return [
{ label: '总申请次数', value: m12.total || 0, unit: '次' },
{ label: '总申请机构数', value: orgTotal || 0, unit: '家' },
{ label: '银行申请次数', value: m12.bank || 0, unit: '次' },
{ label: '非银申请次数', value: m12.nbank || 0, unit: '次' },
{ label: '夜间申请次数', value: nightTotal || 0, unit: '次' },
{ label: '周末申请次数', value: weekendTotal || 0, unit: '次' },
]
})
</script>
<style scoped>
.card {
background: #ffffff;
}
</style>

128
src/ui/JRZQ6F2A/index.vue Normal file
View File

@@ -0,0 +1,128 @@
<template>
<div class="flex flex-col gap-4">
<!-- 统计概览小模块 -->
<SummaryApplyStats :data="variableValue" />
<!-- 申请次数 -->
<ApplicationCountSection :data="variableValue" />
<!-- 产品类型申请分布 -->
<ProductTypeDistributionSection :data="variableValue" />
<!-- 近期集中申请提示 -->
<RecentIntensiveApplicationSection :data="variableValue" />
<!-- 申请总次数 (银行+非银) -->
<ApplicationTotalSection :data="variableValue" />
<!-- 申请机构总数 (银行+非银) -->
<InstitutionTotalSection :data="variableValue" />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { extractVariableValue } from './utils/dataParser'
import ApplicationCountSection from './components/ApplicationCountSection.vue'
import ApplicationTotalSection from './components/ApplicationTotalSection.vue'
import InstitutionTotalSection from './components/InstitutionTotalSection.vue'
import SummaryApplyStats from './components/SummaryApplyStats.vue'
import ProductTypeDistributionSection from './components/ProductTypeDistributionSection.vue'
import RecentIntensiveApplicationSection from './components/RecentIntensiveApplicationSection.vue'
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({})
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
// 获取数据
const rawData = computed(() => props.data?.data || props.data || {})
// 提取 variableValue
const variableValue = computed(() => extractVariableValue(rawData.value))
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
const data = variableValue.value
if (!data || Object.keys(data).length === 0) {
return 100 // 无数据视为最安全
}
let score = 100 // 初始满分
// 近7天申请次数评估
const d7Total = parseInt(data.als_d7_id_bank_allnum || 0) + parseInt(data.als_d7_id_nbank_allnum || 0)
if (d7Total > 5) {
score -= 20 // 近7天申请过多
} else if (d7Total > 3) {
score -= 10
}
// 近15天申请次数评估
const d15Total = parseInt(data.als_d15_id_bank_allnum || 0) + parseInt(data.als_d15_id_nbank_allnum || 0)
if (d15Total > 10) {
score -= 15
} else if (d15Total > 5) {
score -= 8
}
// 近1个月申请次数评估
const m1Total = parseInt(data.als_m1_id_bank_allnum || 0) + parseInt(data.als_m1_id_nbank_allnum || 0)
if (m1Total > 15) {
score -= 20
} else if (m1Total > 8) {
score -= 10
}
// 近3个月申请次数评估
const m3Total = parseInt(data.als_m3_id_bank_allnum || 0) + parseInt(data.als_m3_id_nbank_allnum || 0)
if (m3Total > 20) {
score -= 15
} else if (m3Total > 10) {
score -= 8
}
// 近6个月申请次数评估
const m6Total = parseInt(data.als_m6_id_bank_allnum || 0) + parseInt(data.als_m6_id_nbank_allnum || 0)
if (m6Total > 30) {
score -= 10
} else if (m6Total > 15) {
score -= 5
}
// 确保分数在10-100范围内
return Math.max(10, Math.min(100, score))
})
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore)
// 暴露给父组件
defineExpose({
riskScore
})
</script>
<style scoped>
.card {
background: #ffffff;
border: 1px solid #e5e7eb;
}
</style>

View File

@@ -0,0 +1,237 @@
/**
* 数据解析工具函数
* 用于解析借贷申请验证A的返回数据
*/
/**
* 获取字段值,处理空值
*/
export function getValue(value) {
if (
value === undefined ||
value === null ||
value === "" ||
value === "空"
) {
return 0;
}
// 如果是字符串数字,转换为数字
if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value)) {
return parseFloat(value);
}
return value;
}
/**
* 从原始数据中提取 variableValue
*/
export function extractVariableValue(data) {
try {
return (
data?.risk_screen_v2?.variables?.find(
(v) => v.variableName === "bairong_applyloan_extend"
)?.variableValue || {}
);
} catch (error) {
console.error("提取数据失败:", error);
return {};
}
}
/**
* 时间段映射
*/
export const PERIOD_MAP = {
d7: { label: "7天", prefix: "als_d7" },
d15: { label: "15天", prefix: "als_d15" },
m1: { label: "1个月", prefix: "als_m1" },
m3: { label: "3个月", prefix: "als_m3" },
m6: { label: "6个月", prefix: "als_m6" },
m12: { label: "12个月", prefix: "als_m12" },
};
/**
* 获取申请次数(按时间段)
*/
export function getApplicationCounts(variableValue, period) {
const { prefix } = PERIOD_MAP[period];
// 计算总申请次数(所有类型的申请次数之和)
const types = [
"id_pdl_allnum", // 线上小额现金贷
"id_caon_allnum", // 线上现金分期
"id_rel_allnum", // 信用卡(类信用卡)
"id_caoff_allnum", // 线下现金分期
"id_cooff_allnum", // 线下消费分期
"id_af_allnum", // 汽车金融
"id_coon_allnum", // 线上消费分期
"id_oth_allnum", // 其他
];
let total = 0;
types.forEach((type) => {
const value = getValue(variableValue[`${prefix}_${type}`]);
total += value;
});
// 银行机构申请次数
const bankTotal = getValue(variableValue[`${prefix}_id_bank_allnum`]) || 0;
// 非银机构申请次数
const nbankTotal =
getValue(variableValue[`${prefix}_id_nbank_allnum`]) || 0;
// 如果计算出的total为0则使用银行+非银的总和
const finalTotal = total > 0 ? total : bankTotal + nbankTotal;
return {
total: finalTotal,
bank: bankTotal,
nbank: nbankTotal,
};
}
/**
* 获取特殊时段(周末/夜间)申请次数(按时间段,银行+非银)
*/
export function getSpecialPeriodCounts(variableValue, period) {
const { prefix } = PERIOD_MAP[period];
const weekendBank =
getValue(variableValue[`${prefix}_id_bank_week_allnum`]) || 0;
const weekendNbank =
getValue(variableValue[`${prefix}_id_nbank_week_allnum`]) || 0;
const nightBank =
getValue(variableValue[`${prefix}_id_bank_night_allnum`]) || 0;
const nightNbank =
getValue(variableValue[`${prefix}_id_nbank_night_allnum`]) || 0;
return {
weekend: weekendBank + weekendNbank,
night: nightBank + nightNbank,
};
}
/**
* 获取银行机构申请次数详情
*/
export function getBankApplicationDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period];
return {
pdl: getValue(variableValue[`${prefix}_id_pdl_allnum`]), // 线上小额现金贷
caon: getValue(variableValue[`${prefix}_id_caon_allnum`]), // 线上现金分期
rel: getValue(variableValue[`${prefix}_id_rel_allnum`]), // 信用卡(类信用卡)
caoff: getValue(variableValue[`${prefix}_id_caoff_allnum`]), // 线下现金分期
cooff: getValue(variableValue[`${prefix}_id_cooff_allnum`]), // 线下消费分期
af: getValue(variableValue[`${prefix}_id_af_allnum`]), // 汽车金融
coon: getValue(variableValue[`${prefix}_id_coon_allnum`]), // 线上消费分期
oth: getValue(variableValue[`${prefix}_id_oth_allnum`]), // 其他
bank: getValue(variableValue[`${prefix}_id_bank_allnum`]), // 银行机构申请
tra: getValue(variableValue[`${prefix}_id_bank_tra_allnum`]), // 传统银行申请
ret: getValue(variableValue[`${prefix}_id_bank_ret_allnum`]), // 网络零售银行申请
};
}
/**
* 获取非银机构申请次数详情
*/
export function getNBankApplicationDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period];
return {
nbank: getValue(variableValue[`${prefix}_id_nbank_allnum`]), // 非银机构
p2p: getValue(variableValue[`${prefix}_id_nbank_p2p_allnum`]), // 改制机构
mc: getValue(variableValue[`${prefix}_id_nbank_mc_allnum`]), // 小贷机构
ca: getValue(variableValue[`${prefix}_id_nbank_ca_allnum`]), // 现金类分期机构
cf: getValue(variableValue[`${prefix}_id_nbank_cf_allnum`]), // 消费类分期机构
com: getValue(variableValue[`${prefix}_id_nbank_com_allnum`]), // 代偿类分期机构
oth: getValue(variableValue[`${prefix}_id_nbank_oth_allnum`]), // 其他申请
nsloan: getValue(variableValue[`${prefix}_id_nbank_nsloan_allnum`]), // 持牌网络小贷机构
autofin: getValue(variableValue[`${prefix}_id_nbank_autofin_allnum`]), // 持牌汽车金融机构
sloan: getValue(variableValue[`${prefix}_id_nbank_sloan_allnum`]), // 持牌小贷机构
cons: getValue(variableValue[`${prefix}_id_nbank_cons_allnum`]), // 持牌消费金融机构
finlea: getValue(variableValue[`${prefix}_id_nbank_finlea_allnum`]), // 持牌融资租赁机构
else: getValue(variableValue[`${prefix}_id_nbank_else_allnum`]), // 其他申请
};
}
/**
* 获取银行机构申请机构数详情
*/
export function getBankOrgDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period];
return {
pdl: getValue(variableValue[`${prefix}_id_pdl_orgnum`]),
caon: getValue(variableValue[`${prefix}_id_caon_orgnum`]),
rel: getValue(variableValue[`${prefix}_id_rel_orgnum`]),
caoff: getValue(variableValue[`${prefix}_id_caoff_orgnum`]),
cooff: getValue(variableValue[`${prefix}_id_cooff_orgnum`]),
af: getValue(variableValue[`${prefix}_id_af_orgnum`]),
coon: getValue(variableValue[`${prefix}_id_coon_orgnum`]),
oth: getValue(variableValue[`${prefix}_id_oth_orgnum`]),
bank: getValue(variableValue[`${prefix}_id_bank_orgnum`]),
tra: getValue(variableValue[`${prefix}_id_bank_tra_orgnum`]),
ret: getValue(variableValue[`${prefix}_id_bank_ret_orgnum`]),
};
}
/**
* 获取非银机构申请机构数详情
*/
export function getNBankOrgDetails(variableValue, period) {
const { prefix } = PERIOD_MAP[period];
return {
nbank: getValue(variableValue[`${prefix}_id_nbank_orgnum`]),
p2p: getValue(variableValue[`${prefix}_id_nbank_p2p_orgnum`]),
mc: getValue(variableValue[`${prefix}_id_nbank_mc_orgnum`]),
ca: getValue(variableValue[`${prefix}_id_nbank_ca_orgnum`]),
cf: getValue(variableValue[`${prefix}_id_nbank_cf_orgnum`]),
com: getValue(variableValue[`${prefix}_id_nbank_com_orgnum`]),
oth: getValue(variableValue[`${prefix}_id_nbank_oth_orgnum`]),
nsloan: getValue(variableValue[`${prefix}_id_nbank_nsloan_orgnum`]),
autofin: getValue(variableValue[`${prefix}_id_nbank_autofin_orgnum`]),
sloan: getValue(variableValue[`${prefix}_id_nbank_sloan_orgnum`]),
cons: getValue(variableValue[`${prefix}_id_nbank_cons_orgnum`]),
finlea: getValue(variableValue[`${prefix}_id_nbank_finlea_orgnum`]),
else: getValue(variableValue[`${prefix}_id_nbank_else_orgnum`]),
};
}
/**
* 字段名称映射
*/
export const FIELD_LABELS = {
// 银行机构申请类型
bank: {
pdl: "申请线上小额现金贷",
caon: "申请线上现金分期",
rel: "申请信用卡(类信用卡)",
caoff: "申请线下现金分期",
cooff: "申请线下消费分期",
af: "申请汽车金融",
coon: "申请线上消费分期",
oth: "申请其他",
bank: "银行机构申请",
tra: "银行机构-传统银行申请",
ret: "银行机构-网络零售银行申请",
},
// 非银机构申请类型
nbank: {
nbank: "非银机构",
p2p: "改制机构",
mc: "小贷机构",
ca: "现金类分期机构",
cf: "消费类分期机构",
com: "代偿类分期机构",
oth: "其他申请",
nsloan: "持牌网络小贷机构",
autofin: "汽车金融",
sloan: "持牌小贷机构",
cons: "持牌消费金融机构",
finlea: "持牌融资租赁机构",
else: "其他申请",
},
};

View File

@@ -1,292 +1,360 @@
<template> <template>
<div class="p-4 bg-gradient-to-b from-gray-50/50 to-gray-100/30 min-h-screen"> <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="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-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="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<div class="flex items-center"> <div class="flex items-center">
<van-icon name="balance-list" class="text-xl mr-2" style="color: var(--color-warning);" /> <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> <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> </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>
<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="grid grid-cols-3 gap-2 mb-6"> <div class="text-sm mt-1" style="color: var(--van-theme-primary);">今日新增</div>
<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>
<div class="text-center p-2 rounded-lg bg-white/50">
<div class="grid grid-cols-2 gap-4 mb-6"> <div class="text-lg font-bold" style="color: #8b5cf6;">{{ teamStats?.month_new_members || 0 }}</div>
<div class="p-3 rounded-lg backdrop-blur-sm" style="background-color: rgba(245, 158, 11, 0.08);"> <div class="text-sm mt-1" style="color: var(--van-theme-primary);">本月新增</div>
<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> </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);"> <div class="grid grid-cols-4 gap-3 mb-4">
<van-icon name="description" class="mr-1" />有效报告 <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>
<div class="text-xl font-bold mt-1" style="color: var(--color-warning);"> <div class="text-center p-2 rounded-lg bg-white/50">
{{ currentPromoteData.report || 0 }} <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>
<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>
</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="flex items-center justify-between text-sm font-semibold cursor-pointer pt-4" <div class="text-sm mt-1" style="color: var(--van-theme-primary);">普通下级</div>
style="color: var(--color-warning);" @click="goToPromoteDetail">
<span>查看收益明细</span>
<span class="text-lg"></span>
</div> </div>
</div> </div>
<!-- 活跃下级奖励 -->
<div class="rounded-xl shadow-lg bg-gradient-to-r from-success-50/50 to-success-100/40 p-6">
<div class="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"> <div class="mt-4">
<button @click="toSubordinateList" <button @click="toTeamList"
class="w-full text-white rounded-full py-2 px-4 shadow-md flex items-center justify-center bg-success" class="w-full text-white rounded-full py-2 px-4 shadow-md flex items-center justify-center"
style="background: linear-gradient(135deg, var(--color-success), var(--color-success-600));"> style="background: linear-gradient(135deg, #8b5cf6, #7c3aed);">
<van-icon name="friends" class="mr-1" /> <van-icon name="friends" class="mr-1" />
查看我的下级 查看我的团队
</button> </button>
</div> </div>
</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> </div>
</template> </template>
<script setup> <script setup>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { ref, computed } from "vue"; import { ref, computed, onMounted, watch } from "vue";
import { useRouter } from "vue-router"; 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 agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore); const { isAgent } = storeToRefs(agentStore);
const userStore = useUserStore();
const { isLoggedIn } = storeToRefs(userStore);
const router = useRouter(); const router = useRouter();
const data = ref(null); const teamStats = ref(null);
const conversionData = ref(null);
// 日期选项映射 const myConversionActiveTab = ref('daily');
const dateRangeMap = { const subordinateConversionActiveTab = ref('daily');
today: "today", const isLoadingConversion = ref(true);
week: "last7d", const hasLoaded = ref(false);
month: "last30d",
};
// 直推报告数据
const promoteDateOptions = [
{ label: "今日", value: "today" },
{ label: "近7天", value: "week" },
{ label: "近1月", value: "month" },
];
const selectedPromoteDate = ref("today");
// 活跃下级数据
const activeDateOptions = [
{ label: "今日", value: "today" },
{ label: "近7天", value: "week" },
{ label: "近1月", value: "month" },
];
const selectedActiveDate = ref("today");
// 计算当前直推数据
const currentPromoteData = computed(() => {
const range = dateRangeMap[selectedPromoteDate.value];
return data.value?.direct_push?.[range] || { commission: 0, report: 0 };
});
// 计算当前活跃数据
const currentActiveData = computed(() => {
const range = dateRangeMap[selectedActiveDate.value];
return (
data.value?.active_reward?.[range] || {
active_reward: 0,
sub_promote_reward: 0,
sub_upgrade_reward: 0,
sub_withdraw_reward: 0,
}
);
});
const getData = async () => { const getData = async () => {
const { data: res, error } = await useApiFetch("/agent/revenue") // 获取团队统计
.get() const { data: teamData, error: teamError } = await getTeamStatistics();
.json(); if (teamData.value?.code === 200 && !teamError.value) {
teamStats.value = teamData.value.data;
if (res.value?.code === 200 && !error.value) {
data.value = res.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(() => { 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(); getData();
} }
}); });
// 路由跳转 // 添加跳转到团队列表的方法
const goToPromoteDetail = () => router.push({ name: "promoteDetails" }); const toTeamList = () => {
const goToActiveDetail = () => router.push({ name: "rewardsDetails" }); router.push("/agent/teamList");
};
const toWithdraw = () => router.push({ name: "withdraw" }); const toPromotionQueryList = () => {
router.push({ name: "agentPromotionQueryList" });
};
const toWithdrawDetails = () => router.push({ name: "withdrawDetails" }); const toLogin = () => {
router.push("/login");
// 添加跳转到下级列表的方法
const toSubordinateList = () => {
router.push("/agent/subordinateList");
}; };
</script> </script>
<style> <style></style>
/* 添加按钮悬停效果 */
button {
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-1px);
}
</style>

View File

@@ -7,19 +7,24 @@
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span> <span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
<span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span> <span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span>
</div> </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" <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.product_name)"> :class="getReportTypeStyle(item.product_name)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.product_name)"></span> <span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.product_name)"></span>
{{ item.product_name }} {{ item.product_name }}
</span> </span>
</div> </div>
<div v-if="item.order_no" class="text-xs text-gray-400">
订单号{{ item.order_no }}
</div>
</div> </div>
</van-list> </van-list>
</div> </div>
</template> </template>
<script setup> <script setup>
import { getCommissionList } from '@/api/agent'
// 颜色配置(根据产品名称映射) // 颜色配置(根据产品名称映射)
const typeColors = { const typeColors = {
'小微企业': { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' }, '小微企业': { 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-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' },
'贷前风险': { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-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-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' } 'default': { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' }
} }
@@ -63,11 +68,14 @@ const onLoad = async () => {
// 获取数据 // 获取数据
const getData = async () => { const getData = async () => {
if (loading.value || finished.value) return
try { try {
loading.value = true loading.value = true
const { data: res, error } = await useApiFetch( const { data: res, error } = await getCommissionList({
`/agent/commission?page=${page.value}&page_size=${pageSize.value}` page: page.value,
).get().json() page_size: pageSize.value
})
if (res.value?.code === 200 && !error.value) { if (res.value?.code === 200 && !error.value) {
// 首次加载 // 首次加载
@@ -83,7 +91,15 @@ const getData = async () => {
res.value.data.list.length < pageSize.value) { res.value.data.list.length < pageSize.value) {
finished.value = true finished.value = true
} }
} else {
// 接口返回错误或请求失败,停止翻页
finished.value = true
console.error('获取佣金列表失败:', res.value?.msg || error.value || '未知错误')
} }
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取佣金列表失败:', err)
} finally { } finally {
loading.value = false 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> <template>
<div class="min-h-screen bg-gray-50"> <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"> <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 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-gray-500 text-sm">{{ item.create_time || '-' }}</span>
<span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span> <span class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</span>
</div> </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" <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
:class="getReportTypeStyle(item.type)"> :class="getReportTypeStyle(item.rebate_type)">
<span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.type)"></span> <span class="w-2 h-2 rounded-full mr-1" :class="getDotColor(item.rebate_type)"></span>
{{ typeToChinese(item.type) }} {{ typeToChinese(item.rebate_type) }}
</span> </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>
</div> </div>
</van-list> </van-list>
@@ -20,38 +49,56 @@
</template> </template>
<script setup> <script setup>
// 类型映射配置 import { ref, onMounted } from 'vue'
import { getRebateList, getUpgradeRebateList } from '@/api/agent'
// 返佣类型映射配置(推广返佣)
const typeConfig = { const typeConfig = {
descendant_promotion: { 1: {
chinese: '下级推广奖励', chinese: '直接上级返佣',
color: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' } color: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' }
}, },
descendant_upgrade_vip: { 2: {
chinese: '下级升级VIP奖励', chinese: '钻石上级返佣',
color: { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' }
},
descendant_upgrade_svip: {
chinese: '下级升级SVIP奖励',
color: { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' } color: { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' }
}, },
descendant_stay_activedescendant: { 3: {
chinese: '下级活跃奖励', chinese: '黄金上级返佣',
color: { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' } color: { bg: 'bg-yellow-100', text: 'text-yellow-800', dot: 'bg-yellow-500' }
},
new_active: {
chinese: '新增活跃奖励',
color: { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' }
},
descendant_withdraw: {
chinese: '下级提现奖励',
color: { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' }
}, },
default: { default: {
chinese: '其他奖励', chinese: '其他返佣',
color: { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' } 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 page = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
const data = ref({ const data = ref({
@@ -62,19 +109,55 @@ const loading = ref(false)
const finished = ref(false) const finished = ref(false)
// 类型转中文 // 类型转中文
const typeToChinese = (type) => { const typeToChinese = (rebateType) => {
return typeConfig[type]?.chinese || typeConfig.default.chinese return typeConfig[rebateType]?.chinese || typeConfig.default.chinese
} }
// 获取颜色样式 // 获取颜色样式
const getReportTypeStyle = (type) => { const getReportTypeStyle = (rebateType) => {
const config = typeConfig[type] || typeConfig.default const config = typeConfig[rebateType] || typeConfig.default
return `${config.color.bg} ${config.color.text}` return `${config.color.bg} ${config.color.text}`
} }
// 获取小圆点颜色 // 获取小圆点颜色
const getDotColor = (type) => { const getDotColor = (rebateType) => {
return typeConfig[type]?.color.dot || typeConfig.default.color.dot 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 () => { const getData = async () => {
if (loading.value || finished.value) return
try { try {
loading.value = true loading.value = true
const { data: res, error } = await useApiFetch(
`/agent/rewards?page=${page.value}&page_size=${pageSize.value}` const params = {
).get().json() 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 (res.value?.code === 200 && !error.value) {
if (page.value === 1) { let list = res.value.data.list || []
data.value = res.value.data let total = res.value.data.total
} else {
data.value.list.push(...res.value.data.list) // 升级返佣需要转换数据格式
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 || if (page.value === 1) {
res.value.data.list.length < pageSize.value) { 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 finished.value = true
} }
} else {
// 接口返回错误或请求失败,停止翻页
finished.value = true
console.error('获取返佣列表失败:', res.value?.msg || error.value || '未知错误')
} }
} catch (err) {
// 捕获异常,停止翻页
finished.value = true
console.error('获取返佣列表失败:', err)
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -0,0 +1,530 @@
<template>
<div class="agent-system-guide min-h-screen" style="background-color: #f7f8fa;">
<div class="px-4 pt-6 pb-4">
<h1 class="text-2xl font-bold mb-1" style="color: var(--van-text-color);">代理系统指南</h1>
<p class="text-sm" style="color: var(--van-text-color-2);">了解代理政策等级特权与收益计算</p>
</div>
<div class="px-4 pb-6 space-y-4">
<!-- 代理政策概述 -->
<section class="bg-white rounded-lg p-4" style="box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<div class="flex items-center mb-3">
<div class="w-1 h-5 rounded-full mr-2" style="background-color: var(--van-theme-primary);"></div>
<h2 class="text-lg font-semibold" style="color: var(--van-text-color);">代理政策概述</h2>
</div>
<div class="space-y-2.5 text-sm leading-relaxed" style="color: var(--van-text-color-2);">
<p>
全能查代理系统采用三级代理体系为合作伙伴提供多层级多收益的推广模式
通过推广平台产品和服务代理可获得推广佣金下级返佣升级返佣等多种收益
</p>
<p>
系统分为<strong>普通代理</strong><strong>黄金代理</strong><strong>钻石代理</strong>三个等级
不同等级享有不同的权限和收益比例
</p>
<p class="text-sm mt-2" style="color: var(--van-text-color-3);">
详细政策请查看代理管理协议
</p>
</div>
</section>
<!-- 代理等级说明 -->
<section class="bg-white rounded-lg p-4" style="box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<div class="flex items-center mb-4">
<div class="w-1 h-5 rounded-full mr-2" style="background-color: var(--van-theme-primary);"></div>
<h2 class="text-lg font-semibold" style="color: var(--van-text-color);">代理等级体系</h2>
</div>
<div class="space-y-4">
<!-- 普通代理 -->
<div class="border rounded-lg p-4 mb-3" style="border-color: #e5e7eb; background-color: #fafafa;">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div
class="w-10 h-10 rounded-full bg-gray-400 flex items-center justify-center text-white font-semibold text-base mr-3">
1
</div>
<div>
<h3 class="text-base font-semibold" style="color: var(--van-text-color);">普通代理</h3>
<p class="text-sm" style="color: var(--van-text-color-2);">基础代理特权</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
style="background-color: #f3f4f6; color: #6b7280;">
入门级
</span>
</div>
<div class="space-y-1.5 text-sm" style="color: var(--van-text-color-2);">
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5"></span>
<span>推广产品获得推广佣金</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5"></span>
<span>邀请下级代理获得下级推广返佣</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5"></span>
<span>查看团队统计和转化率数据</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5"></span>
<span>生成和管理邀请码</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5"></span>
<span>申请升级为黄金或钻石代理</span>
</div>
</div>
</div>
<!-- 黄金代理 -->
<div class="border rounded-lg p-4 mb-3" style="border-color: #fbbf24; background-color: #fffbeb;">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold text-base mr-3"
style="background: linear-gradient(135deg, #f59e0b, #d97706);">
2
</div>
<div>
<h3 class="text-base font-semibold" style="color: #92400e;">黄金代理</h3>
<p class="text-sm" style="color: #b45309;">高级代理特权</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
style="background-color: #fef3c7; color: #92400e;">
进阶级
</span>
</div>
<div class="space-y-1.5 text-sm" style="color: var(--van-text-color-2);">
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5"></span>
<span class="font-medium">享有普通代理所有权限</span>
</div>
<div class="flex items-start">
<span class="text-yellow-500 mr-1.5 mt-0.5"></span>
<span>更高的推广佣金比例</span>
</div>
<div class="flex items-start">
<span class="text-yellow-500 mr-1.5 mt-0.5"></span>
<span>更高的下级推广返佣比例</span>
</div>
<div class="flex items-start">
<span class="text-yellow-500 mr-1.5 mt-0.5"></span>
<span>获得下级升级返佣当普通下级升级为黄金时</span>
</div>
<div class="flex items-start">
<span class="text-yellow-500 mr-1.5 mt-0.5"></span>
<span>可申请升级为钻石代理</span>
</div>
</div>
</div>
<!-- 钻石代理 -->
<div class="border rounded-lg p-4" style="border-color: #a855f7; background-color: #faf5ff;">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold text-base mr-3"
style="background: linear-gradient(135deg, #9333ea, #7c3aed);">
💎
</div>
<div>
<h3 class="text-base font-semibold" style="color: #6b21a8;">钻石代理</h3>
<p class="text-sm" style="color: #7c3aed;">尊享代理特权</p>
</div>
</div>
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
style="background-color: #e9d5ff; color: #6b21a8;">
最高级
</span>
</div>
<div class="space-y-1.5 text-sm" style="color: var(--van-text-color-2);">
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5"></span>
<span class="font-medium">享有黄金代理所有权限</span>
</div>
<div class="flex items-start">
<span class="text-purple-500 mr-1.5 mt-0.5"></span>
<span>最高推广佣金比例</span>
</div>
<div class="flex items-start">
<span class="text-purple-500 mr-1.5 mt-0.5"></span>
<span>最高下级推广返佣比例</span>
</div>
<div class="flex items-start">
<span class="text-purple-500 mr-1.5 mt-0.5"></span>
<span>获得下级升级返佣当普通/黄金下级升级时</span>
</div>
<div class="flex items-start">
<span class="text-purple-500 mr-1.5 mt-0.5"></span>
<span class="font-medium">可调整下级代理等级将普通代理升级为黄金代理</span>
</div>
<div class="flex items-start">
<span class="text-purple-500 mr-1.5 mt-0.5"></span>
<span>专属客服支持</span>
</div>
</div>
</div>
</div>
</section>
<!-- 收益计算说明 -->
<section class="bg-white rounded-lg p-4" style="box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<div class="flex items-center mb-4">
<div class="w-1 h-5 rounded-full mr-2" style="background-color: var(--van-theme-primary);"></div>
<h2 class="text-lg font-semibold" style="color: var(--van-text-color);">收益计算方式</h2>
</div>
<div class="space-y-3">
<!-- 推广佣金 -->
<div class="rounded-lg p-3 border-l-2" style="background-color: #eff6ff; border-color: #3b82f6;">
<div class="flex items-center mb-2">
<van-icon name="gold-coin" class="text-base mr-2" style="color: #3b82f6;" />
<h3 class="text-base font-semibold" style="color: #1e40af;">推广佣金</h3>
</div>
<p class="text-sm mb-2.5" style="color: var(--van-text-color-2);">
当您推广的产品被用户购买并支付成功后您将获得推广佣金
</p>
<div class="bg-white rounded p-2.5 text-sm">
<div class="font-medium mb-1.5" style="color: var(--van-text-color);">计算公式</div>
<div class="mb-1.5" style="color: var(--van-text-color-2);">
推广佣金 = 订单金额 × 佣金比例
</div>
<div class="text-sm" style="color: var(--van-text-color-3);">
佣金比例根据您的代理等级和产品类型而定等级越高佣金比例越高
</div>
</div>
</div>
<!-- 下级推广返佣 -->
<div class="rounded-lg p-3 border-l-2" style="background-color: #f0fdf4; border-color: #22c55e;">
<div class="flex items-center mb-2">
<van-icon name="gift-o" class="text-base mr-2" style="color: #22c55e;" />
<h3 class="text-base font-semibold" style="color: #15803d;">下级推广返佣</h3>
</div>
<p class="text-sm mb-2.5" style="color: var(--van-text-color-2);">
当您的下级代理推广产品产生订单时您将获得一定比例的返佣
</p>
<div class="bg-white rounded p-2.5 text-sm">
<div class="font-medium mb-1.5" style="color: var(--van-text-color);">返佣规则</div>
<ul class="space-y-1 mb-1.5" style="color: var(--van-text-color-2);">
<li> 直接下级获得直接下级推广订单的返佣</li>
<li> 间接下级获得间接下级推广订单的返佣</li>
<li> 返佣比例根据您的代理等级而定</li>
</ul>
<div class="text-sm" style="color: var(--van-text-color-3);">
返佣金额 = 下级订单金额 × 返佣比例
</div>
</div>
</div>
<!-- 下级升级返佣 -->
<div class="rounded-lg p-3 border-l-2" style="background-color: #faf5ff; border-color: #a855f7;">
<div class="flex items-center mb-2">
<van-icon name="arrow-up" class="text-base mr-2" style="color: #a855f7;" />
<h3 class="text-base font-semibold" style="color: #6b21a8;">下级升级返佣</h3>
</div>
<p class="text-sm mb-2.5" style="color: var(--van-text-color-2);">
当您的下级代理付费升级为更高等级时您将获得升级返佣
</p>
<div class="bg-white rounded p-2.5 text-sm">
<div class="font-medium mb-1.5" style="color: var(--van-text-color);">返佣规则</div>
<ul class="space-y-1 mb-1.5" style="color: var(--van-text-color-2);">
<li> 普通代理升级为黄金代理上级获得返佣</li>
<li> 普通/黄金代理升级为钻石代理上级获得返佣</li>
<li> 返佣金额由系统配置在升级时显示</li>
</ul>
<div class="text-sm" style="color: var(--van-text-color-3);">
只有黄金代理和钻石代理才能获得下级升级返佣
</div>
</div>
</div>
<!-- 收益统计 -->
<div class="rounded-lg p-3 border-l-2" style="background-color: #fff7ed; border-color: #f97316;">
<div class="flex items-center mb-2">
<van-icon name="balance-list" class="text-base mr-2" style="color: #f97316;" />
<h3 class="text-base font-semibold" style="color: #c2410c;">收益统计</h3>
</div>
<div class="bg-white rounded p-2.5 text-sm" style="color: var(--van-text-color-2);">
<p class="mb-1.5">您可以在"我的"页面查看详细的收益统计</p>
<ul class="space-y-1">
<li> <span class="font-medium">累计总收益</span>所有收益的总和</li>
<li> <span class="font-medium">今日收益</span>当天的收益金额</li>
<li> <span class="font-medium">本月收益</span>本月的收益金额</li>
<li> <span class="font-medium">余额</span>可提现的金额</li>
<li> <span class="font-medium">风险保障金</span>冻结的保障金</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 功能玩法 -->
<section class="bg-white rounded-lg p-4" style="box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<div class="flex items-center mb-4">
<div class="w-1 h-5 rounded-full mr-2" style="background-color: var(--van-theme-primary);"></div>
<h2 class="text-lg font-semibold" style="color: var(--van-text-color);">功能玩法</h2>
</div>
<div class="grid grid-cols-2 gap-3">
<!-- 推广查询 -->
<div class="rounded-lg p-3 border" style="background-color: #eff6ff; border-color: #bfdbfe;">
<div class="flex items-center mb-1.5">
<van-icon name="share-o" class="text-sm mr-1.5" style="color: #3b82f6;" />
<h3 class="font-semibold text-sm" style="color: #1e40af;">推广查询</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
生成推广链接分享给用户购买查询服务获得推广佣金
</p>
</div>
<!-- 邀请下级 -->
<div class="rounded-lg p-3 border" style="background-color: #f0fdf4; border-color: #bbf7d0;">
<div class="flex items-center mb-1.5">
<van-icon name="friends-o" class="text-sm mr-1.5" style="color: #22c55e;" />
<h3 class="font-semibold text-sm" style="color: #15803d;">邀请下级</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
通过邀请码邀请他人成为您的下级代理获得下级推广返佣
</p>
</div>
<!-- 团队管理 -->
<div class="rounded-lg p-3 border" style="background-color: #faf5ff; border-color: #e9d5ff;">
<div class="flex items-center mb-1.5">
<van-icon name="manager-o" class="text-sm mr-1.5" style="color: #a855f7;" />
<h3 class="font-semibold text-sm" style="color: #6b21a8;">团队管理</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
查看团队统计转化率数据了解团队发展情况
</p>
</div>
<!-- 升级代理 -->
<div class="rounded-lg p-3 border" style="background-color: #fffbeb; border-color: #fde68a;">
<div class="flex items-center mb-1.5">
<van-icon name="arrow-up" class="text-sm mr-1.5" style="color: #f59e0b;" />
<h3 class="font-semibold text-sm" style="color: #92400e;">升级代理</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
付费升级为更高等级享受更高佣金和返佣比例
</p>
</div>
<!-- 邀请码管理 -->
<div class="rounded-lg p-3 border" style="background-color: #eef2ff; border-color: #c7d2fe;">
<div class="flex items-center mb-1.5">
<van-icon name="qr" class="text-sm mr-1.5" style="color: #6366f1;" />
<h3 class="font-semibold text-sm" style="color: #4338ca;">邀请码管理</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
生成查看和管理邀请码用于邀请下级代理
</p>
</div>
<!-- 实名认证 -->
<div class="rounded-lg p-3 border" style="background-color: #fef2f2; border-color: #fecaca;">
<div class="flex items-center mb-1.5">
<van-icon name="passed" class="text-sm mr-1.5" style="color: #ef4444;" />
<h3 class="font-semibold text-sm" style="color: #b91c1c;">实名认证</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
完成实名认证后才能申请提现收益
</p>
</div>
<!-- 提现功能 -->
<div class="rounded-lg p-3 border" style="background-color: #f0fdfa; border-color: #a7f3d0;">
<div class="flex items-center mb-1.5">
<van-icon name="cash" class="text-sm mr-1.5" style="color: #14b8a6;" />
<h3 class="font-semibold text-sm" style="color: #0d9488;">提现功能</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
将收益提现到银行卡需要完成实名认证
</p>
</div>
<!-- 调整级别仅钻石 -->
<div class="rounded-lg p-3 border" style="background-color: #f5f3ff; border-color: #ddd6fe;">
<div class="flex items-center mb-1.5">
<van-icon name="setting-o" class="text-sm mr-1.5" style="color: #8b5cf6;" />
<h3 class="font-semibold text-sm" style="color: #6b21a8;">调整级别</h3>
</div>
<p class="text-sm leading-relaxed" style="color: var(--van-text-color-2);">
<span class="font-medium" style="color: #7c3aed;">钻石代理专属</span>可将普通下级升级为黄金代理
</p>
</div>
</div>
</section>
<!-- 操作指南 -->
<section class="bg-white rounded-lg p-4" style="box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<div class="flex items-center mb-4">
<div class="w-1 h-5 rounded-full mr-2" style="background-color: var(--van-theme-primary);"></div>
<h2 class="text-lg font-semibold" style="color: var(--van-text-color);">操作指南</h2>
</div>
<!-- 如何推广报告 -->
<div class="mb-4">
<div class="flex items-center mb-2.5">
<van-icon name="share-o" class="text-base mr-2" style="color: #3b82f6;" />
<h3 class="text-base font-semibold" style="color: var(--van-text-color);">如何推广报告</h3>
</div>
<div class="space-y-2 text-sm leading-relaxed" style="color: var(--van-text-color-2);">
<div class="flex items-start">
<span class="text-blue-500 mr-1.5 mt-0.5 font-medium">1.</span>
<span>登录代理账户后进入"推广"页面选择要推广的报告类型如个人大数据婚恋风险入职背调等</span>
</div>
<div class="flex items-start">
<span class="text-blue-500 mr-1.5 mt-0.5 font-medium">2.</span>
<span>系统将自动生成专属推广链接您可以将该链接分享给潜在用户用户通过您的推广链接购买查询服务后您即可获得推广佣金</span>
</div>
<div class="flex items-start">
<span class="text-blue-500 mr-1.5 mt-0.5 font-medium">3.</span>
<span>您可以在推广页面设置查询服务的售价需在平台设定的价格范围内售价与成本价的差额即为您的推广收益</span>
</div>
<div class="flex items-start">
<span class="text-blue-500 mr-1.5 mt-0.5 font-medium">4.</span>
<span>推广链接支持多种分享方式包括复制链接生成二维码生成短链等方便您在不同渠道进行推广</span>
</div>
<div class="flex items-start">
<span class="text-blue-500 mr-1.5 mt-0.5 font-medium">5.</span>
<span>您可以在"推广查询记录"中查看所有通过您推广链接产生的订单详情包括订单金额收益金额订单状态等信息</span>
</div>
<div class="flex items-start">
<span class="text-blue-500 mr-1.5 mt-0.5 font-medium">6.</span>
<span>推广佣金将在用户完成支付后自动结算到您的账户余额中您可以在"我的"页面查看收益统计</span>
</div>
</div>
</div>
<!-- 如何邀请下级代理 -->
<div class="border-t pt-4" style="border-color: #e5e7eb;">
<div class="flex items-center mb-2.5">
<van-icon name="friends-o" class="text-base mr-2" style="color: #22c55e;" />
<h3 class="text-base font-semibold" style="color: var(--van-text-color);">如何邀请下级代理</h3>
</div>
<div class="space-y-2 text-sm leading-relaxed" style="color: var(--van-text-color-2);">
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">1.</span>
<span>登录代理账户后进入"邀请码管理"页面系统会为您生成专属邀请码</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">2.</span>
<span>您可以将邀请码分享给想要成为代理的用户用户通过您的邀请码注册成为代理后将自动成为您的下级代理</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">3.</span>
<span>当您的下级代理推广产品产生订单时您将获得一定比例的下级推广返佣返佣比例根据您的代理等级而定等级越高返佣比例越高</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">4.</span>
<span>您可以在"我的团队"页面查看所有下级代理的信息包括直接下级和间接下级以及团队统计数据</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">5.</span>
<span>当您的下级代理付费升级为更高等级时如普通代理升级为黄金代理您将获得下级升级返佣只有黄金代理和钻石代理才能获得下级升级返佣</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">6.</span>
<span>钻石代理拥有特殊权限可以将普通下级代理直接升级为黄金代理无需下级代理付费</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">7.</span>
<span>您可以在"下级推广收益"中查看所有来自下级代理的返佣明细包括推广返佣和升级返佣</span>
</div>
<div class="flex items-start">
<span class="text-green-500 mr-1.5 mt-0.5 font-medium">8.</span>
<span>邀请下级代理是扩大团队规模增加收益的重要方式建议您积极发展下级代理建立稳定的推广团队</span>
</div>
</div>
</div>
</section>
<!-- 提现说明 -->
<section class="bg-white rounded-lg p-4" style="box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<div class="flex items-center mb-3">
<div class="w-1 h-5 rounded-full mr-2" style="background-color: var(--van-theme-primary);"></div>
<h2 class="text-lg font-semibold" style="color: var(--van-text-color);">提现说明</h2>
</div>
<div class="space-y-2.5 text-sm leading-relaxed">
<div class="rounded p-2.5 border-l-2" style="background-color: #fffbeb; border-color: #fbbf24;">
<p class="font-medium mb-1" style="color: #92400e;">提现条件</p>
<ul class="space-y-0.5" style="color: #78350f;">
<li>1. 必须完成实名认证三要素核验</li>
<li>2. 单笔提现金额需达到 50 元及以上</li>
<li>3. 风险保障金需满足平台要求</li>
</ul>
</div>
<div class="rounded p-2.5 border-l-2" style="background-color: #eff6ff; border-color: #60a5fa;">
<p class="font-medium mb-1" style="color: #1e40af;">注意事项</p>
<ul class="space-y-0.5" style="color: #1e3a8a;">
<li> 提现到账账户的实名信息必须与实名认证信息完全一致同一身份证/同一持卡人否则可能提现失败或原路退回</li>
<li> 实名认证时请务必确认使用的证件信息与后续用于收款的账户为同一人</li>
<li> 平台会根据国家税收法律法规代扣代缴个人所得税</li>
<li> 提现申请提交后平台会在规定时间内处理</li>
<li> 可在\"提现记录\"中查看提现状态</li>
</ul>
</div>
</div>
</section>
<!-- 操作按钮 -->
<div class="flex gap-2.5 pb-6">
<button v-if="!isAgent && isLoggedIn" @click="toRegister"
class="flex-1 py-2.5 rounded-lg text-white text-sm font-medium"
style="background-color: var(--van-theme-primary);">
立即注册成为代理
</button>
<button v-if="!isLoggedIn" @click="toLogin"
class="flex-1 py-2.5 rounded-lg text-white text-sm font-medium"
style="background-color: var(--van-theme-primary);">
登录后注册代理
</button>
<button v-if="isAgent && level < 3" @click="toUpgrade"
class="flex-1 py-2.5 rounded-lg text-white text-sm font-medium" style="background-color: #8b5cf6;">
升级代理等级
</button>
<button @click="toAgreement" class="px-4 py-2.5 rounded-lg border text-sm font-medium"
style="border-color: var(--van-theme-primary); color: var(--van-theme-primary);">
查看协议
</button>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import { storeToRefs } from "pinia";
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
const router = useRouter();
const agentStore = useAgentStore();
const userStore = useUserStore();
const { isAgent, level } = storeToRefs(agentStore);
const { isLoggedIn } = storeToRefs(userStore);
const toRegister = () => {
router.push("/register");
};
const toLogin = () => {
router.push("/login");
};
const toUpgrade = () => {
router.push({ name: "agentUpgrade" });
};
const toAgreement = () => {
router.push("/agentManageAgreement");
};
</script>
<style scoped>
.agent-system-guide {
padding-bottom: 1rem;
}
</style>

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

@@ -0,0 +1,444 @@
<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">
<div class="flex justify-between items-start gap-2">
<div>
<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 v-if="getPrivilegeInfo(level)" class="space-y-1 text-sm text-right"
style="color: var(--van-text-color);">
<div class="flex items-center gap-1 justify-end">
<span>设置查询价最高{{ getPrivilegeInfo(level).max_set_price }}</span>
<van-icon name="checked" size="14" color="#f59e0b" />
</div>
<div class="flex items-center gap-1 justify-end">
<span>邀请黄金代理奖励{{ getPrivilegeInfo(level).invite_gold_reward }}</span>
<van-icon name="checked" size="14" color="#f59e0b" />
</div>
<div class="flex items-center gap-1 justify-end">
<span>邀请钻石代理奖励{{ getPrivilegeInfo(level).invite_diamond_reward }}</span>
<van-icon name="checked" size="14" color="#f59e0b" />
</div>
</div>
</div>
</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);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>设置查询价最高{{ getPrivilegeInfo(2).max_set_price }}</span>
</div>
<!-- 查询底价降低 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>查询底价降低{{ formatAmount(getPrivilegeInfo(2).price_reduction) }}元每单</span>
</div>
<!-- 下级代理查询奖励最高 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>下级代理查询奖励最高{{ getPrivilegeInfo(2).subordinate_reward_max }}元每单</span>
</div>
<!-- 邀请黄金代理奖励 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>邀请黄金代理奖励{{ getPrivilegeInfo(2).invite_gold_reward }}</span>
</div>
<!-- 邀请钻石代理奖励 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>邀请钻石代理奖励{{ getPrivilegeInfo(2).invite_diamond_reward }}</span>
</div>
<!-- 更多权益 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>更多权益敬请期待</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);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>升级费用¥{{ formatAmount(getUpgradeInfo(3).upgradeFee) }}</span>
</div>
<!-- 设置查询价最高 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>设置查询价最高{{ getPrivilegeInfo(3).max_set_price }}</span>
</div>
<!-- 查询底价降低 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>查询底价降低{{ formatAmount(getPrivilegeInfo(3).price_reduction) }}元每单</span>
</div>
<!-- 下级代理查询奖励最高 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>下级代理查询奖励最高{{ getPrivilegeInfo(3).subordinate_reward_max }}元每单</span>
</div>
<!-- 邀请黄金代理奖励 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>邀请黄金代理奖励{{ getPrivilegeInfo(3).invite_gold_reward }}</span>
</div>
<!-- 邀请钻石代理奖励 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>邀请钻石代理奖励{{ getPrivilegeInfo(3).invite_diamond_reward }}</span>
</div>
<!-- 调整下级代理级别权限 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>调整下级代理级别权限</span>
</div>
<!-- 系统内白名单权限 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>系统内白名单权限</span>
</div>
<!-- 更多权益 -->
<div class="flex items-center gap-2 text-sm" style="color: var(--van-text-color);">
<van-icon name="checked" size="14" color="#f59e0b" />
<span>更多权益敬请期待</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 formatAmount = (value) => {
const num = Number(value || 0)
if (Number.isNaN(num)) return '0'
return num.toFixed(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 serviceUrl = import.meta.env.VITE_SERVICE_URL || '';
const toService = () => {
if (serviceUrl) {
window.location.href = serviceUrl;
}
}
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(() => { onMounted(() => {
// 插入客服脚本 // 插入客服脚本
(function (d, t) { (function (d, t) {
var BASE_URL = "https://service.quannengcha.com"; var BASE_URL = "https://service.onecha.cn";
var g = d.createElement(t), var g = d.createElement(t),
s = d.getElementsByTagName(t)[0]; s = d.getElementsByTagName(t)[0];
g.src = BASE_URL + "/packs/js/sdk.js"; g.src = BASE_URL + "/packs/js/sdk.js";

View File

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

View File

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

View File

@@ -1,6 +1,16 @@
<script setup> <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 router = useRouter()
const userStore = useUserStore()
const appStore = useAppStore()
const dialogStore = useDialogStore()
const { isLoggedIn, mobile } = storeToRefs(userStore)
const page = ref(1) const page = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
const total = ref(0) const total = ref(0)
@@ -9,9 +19,14 @@ const num = ref(0)
const max = ref(60) const max = ref(60)
const loading = ref(false) const loading = ref(false)
const finished = 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() { async function fetchData() {
if (loading.value || finished.value) return
loading.value = true loading.value = true
try {
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`) const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`)
.get() .get()
.json() .json()
@@ -25,14 +40,27 @@ async function fetchData() {
if (reportList.value.length >= total.value) { if (reportList.value.length >= total.value) {
finished.value = true 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 loading.value = false
}
} }
// 初始加载 // 初始加载
onMounted(() => { onMounted(async () => {
fetchData() fetchData()
}) })
@@ -53,6 +81,19 @@ function toDetail(item) {
router.push({ path: '/report', query: { orderId: item.order_id } }); 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) { function stateText(state) {
switch (state) { switch (state) {
@@ -88,6 +129,20 @@ function statusClass(state) {
<template> <template>
<div class="flex flex-col gap-4 p-4"> <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"> <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="item in reportList" :key="item.id" @click="toDetail(item)" <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"> class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">

View File

@@ -1,16 +1,41 @@
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted, computed } from "vue";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import InquireForm from "@/components/InquireForm.vue"; import { useAgentStore } from "@/stores/agentStore";
import { storeToRefs } from "pinia";
import { showToast } from "vant";
const route = useRoute(); const route = useRoute();
const router = useRouter();
const agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore);
const feature = ref(route.params.feature); const feature = ref(route.params.feature);
// 获取产品信息 // 获取产品信息
const featureData = ref({}); const featureData = ref({});
// 检查是否是代理,如果不是则重定向
onMounted(async () => { onMounted(async () => {
// 检查支付回调
isFinishPayment(); 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(); await getProduct();
}); });
@@ -45,5 +70,18 @@ async function getProduct() {
</script> </script>
<template> <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> </template>

View File

@@ -1,44 +1,398 @@
<template> <template>
<div> <div class="min-h-screen bg-gradient-to-b from-blue-50 to-white pb-20">
<img src="@/assets/images/invitation.png" alt="邀请下级" /> <!-- 页面标题 -->
<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 <div
@click="showQRcode = true" class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center shadow-sm">
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" <span class="text-white text-xs font-bold"></span>
>
立即邀请好友
</div> </div>
</div> </div>
<QRcode <button v-if="selectedCodeId === item.id && item.status === 0"
v-model:show="showQRcode" @click.stop="handleDeleteCode(item.id, item.code)"
mode="invitation" 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">
:linkIdentifier="linkIdentifier" 删除
/> </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>
</template> </template>
<script setup> <script setup>
import { storeToRefs } from "pinia"; import { watch, onMounted } from "vue";
import { aesEncrypt } from "@/utils/crypto"; import { generateInviteCode, getInviteCodeList, deleteInviteCode, getInviteLink } from "@/api/agent";
import { useAgentStore } from "@/stores/agentStore"; import { showToast, showConfirmDialog } from "vant";
const agentStore = useAgentStore(); import QRcode from "@/components/QRcode.vue";
const { mobile, agentID } = storeToRefs(agentStore); // 响应式解构
const showQRcode = ref(false); const showQRcode = ref(false);
const linkIdentifier = ref(""); const inviteLink = ref("");
onBeforeMount(() => { const showInviteLink = ref(false); // 控制是否显示邀请链接区域
encryptIdentifire(agentID.value, mobile.value); 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 = { const selectCode = (item) => {
agentID, selectedCodeId.value = item.id;
mobile, // 清除之前生成的链接和显示状态
}; inviteLink.value = "";
const linkIdentifierStr = JSON.stringify(linkIdentifierJSON); showInviteLink.value = false;
const encodeData = aesEncrypt( };
linkIdentifierStr,
"8e3e7a2f60edb49221e953b9c029ed10" // 生成邀请链接
); const handleGenerateLink = async (code) => {
linkIdentifier.value = encodeURIComponent(encodeData); 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> </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="邀请代理申请" /> <img src="@/assets/images/invitation_agent_apply.png" alt="邀请代理申请" />
<!-- 统一状态处理容器 --> <!-- 统一状态处理容器 -->
<div class="flex flex-col items-center justify-centerx"> <div class="flex flex-col items-center justify-centerx">
<!-- 审核中状态 --> <!-- 已是代理状态 -->
<div v-if="displayStatus === 0" class="text-center"> <div v-if="isAgent" class="text-center">
<span class="text-xs text-gray-500">的申请正在审核中</span> <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 class="bg-green-100 p-1 rounded-3xl shadow-xl mt-1" @click="goToHome"> <div class="bg-green-100 p-1 rounded-3xl shadow-xl mt-1" @click="goToHome">
<div <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"> 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> </div>
<!-- 审核未通过状态 --> <!-- 未成为代理状态包含邀请状态 -->
<div v-if="displayStatus === 2" class="text-center"> <div v-else class="text-center">
<span class="text-xs text-red-500">审核未通过请重新提交</span>
<div class="bg-red-100 p-1 rounded-3xl shadow-xl mt-1" @click="agentApply">
<div
class="text-xl font-bold px-8 py-2 bg-gradient-to-t from-red-500 to-red-300 text-white rounded-3xl shadow-lg cursor-pointer">
重新提交申请
</div>
</div>
</div>
<!-- 未申请状态包含邀请状态 -->
<div v-if="displayStatus === 3" class="text-center">
<span class="text-xs text-gray-500">{{ <span class="text-xs text-gray-500">{{
isSelf ? "立即申请成为代理人" : "邀您注册代理人" isSelf ? "立即申请成为代理人" : "邀您注册代理人"
}}</span> }}</span>
@@ -50,8 +28,7 @@
</div> </div>
</div> </div>
</div> </div>
<AgentApplicationForm v-model:show="showApplyPopup" @submit="submitApplication" @close="showApplyPopup = false" <AgentApplicationForm v-model:show="showApplyPopup" @submit="submitApplication" @close="showApplyPopup = false" />
:ancestor="ancestor" :is-self="isSelf" :user-name="userName" />
</template> </template>
<script setup> <script setup>
@@ -64,63 +41,66 @@ import { ref } from "vue";
const store = useAgentStore(); const store = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();
const { userName } = storeToRefs(userStore); const { userName } = storeToRefs(userStore);
const { status } = storeToRefs(store); // 响应式解构 const { isAgent } = storeToRefs(store);
const ancestor = ref(""); const ancestor = ref("");
const isSelf = ref(false); const isSelf = ref(false);
const agentApply = () => { const agentApply = () => {
showApplyPopup.value = true; showApplyPopup.value = true;
}; };
// 计算显示状态当isSelf为false时强制显示为3
const displayStatus = computed(() => {
// return isSelf.value ? status.value : 3;
return status.value;
});
// 跳转到首页 // 跳转到首页
const goToHome = () => { const goToHome = () => {
clearInterval(intervalId);
router.replace("/promote"); router.replace("/promote");
}; };
onBeforeMount(() => { onBeforeMount(async () => {
if (route.name === "invitationAgentApplySelf") { // 如果是通过邀请链接访问旧的linkIdentifier格式提取信息
isSelf.value = true; if (route.params.linkIdentifier && route.name !== "invitationAgentApplySelf") {
} else { try {
const linkIdentifier = route.params.linkIdentifier; const linkIdentifier = route.params.linkIdentifier;
const decryptDataStr = aesDecrypt( const decryptDataStr = aesDecrypt(
decodeURIComponent(linkIdentifier), decodeURIComponent(linkIdentifier),
"8e3e7a2f60edb49221e953b9c029ed10" "8e3e7a2f60edb49221e953b9c029ed10"
); );
const decryptData = JSON.parse(decryptDataStr); const decryptData = JSON.parse(decryptDataStr);
ancestor.value = decryptData.mobile; // 旧格式可能包含agentID和mobile但新系统使用邀请码
// 这里可以保留兼容,但主要功能是通过邀请码
} catch (error) {
console.error("解析链接标识符失败", error);
} }
} else {
isSelf.value = true;
}
// 检查是否已登录并获取代理状态
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (token) { if (token) {
store.fetchAgentStatus(); await store.fetchAgentStatus();
} }
}); });
import { applyForAgent, registerByInviteCode } from "@/api/agent";
const submitApplication = async (formData) => { const submitApplication = async (formData) => {
// 提交代理申请的数据 const { region, mobile, wechat_id, code, referrer } = formData;
const { region, mobile, wechat_id, code } = formData;
// 根据是否已登录选择不同的API
const isLoggedIn = !!localStorage.getItem("token");
const apiCall = isLoggedIn ? applyForAgent : registerByInviteCode;
let postData = { let postData = {
region, region,
mobile, mobile,
wechat_id, wechat_id,
code, code,
referrer,
}; };
if (!isSelf.value) { const { data, error } = await apiCall(postData);
postData.ancestor = ancestor.value;
}
const { data, error } = await useApiFetch("/agent/apply")
.post(postData)
.json();
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
showApplyPopup.value = false; showApplyPopup.value = false;
showToast({ message: "已提交申请" }); showToast({ message: "注册成功,您已成为代理!" });
// refreshAgentStatus() // 更新token和状态
if (data.value.data.accessToken) { if (data.value.data.accessToken) {
localStorage.setItem("token", data.value.data.accessToken); localStorage.setItem("token", data.value.data.accessToken);
localStorage.setItem( localStorage.setItem(
@@ -131,38 +111,17 @@ const submitApplication = async (formData) => {
"accessExpire", "accessExpire",
data.value.data.accessExpire data.value.data.accessExpire
); );
refreshAgentStatus(); // 重新获取代理状态
await store.fetchAgentStatus();
await userStore.fetchUserInfo();
// 跳转到代理主页
router.replace("/agent");
} }
} else { } else {
console.log("申请失败", data.value); 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> </script>
<style lang="scss" scoped></style> <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> <script setup>
import { ref, computed, onUnmounted, nextTick } from 'vue' import { ref, computed, onUnmounted, nextTick } from 'vue'
import { showToast } from 'vant' 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 router = useRouter()
const route = useRoute()
const agentStore = useAgentStore()
const userStore = useUserStore()
const phoneNumber = ref('') const phoneNumber = ref('')
const verificationCode = ref('') const verificationCode = ref('')
const password = ref('')
const isPasswordLogin = ref(false)
const isAgreed = ref(false)
const isCountingDown = ref(false) const isCountingDown = ref(false)
const countdown = ref(60) const countdown = ref(60)
let timer = null 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(() => { const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(phoneNumber.value) return /^1[3-9]\d{9}$/.test(phoneNumber.value)
}) })
const canLogin = computed(() => { const canLogin = computed(() => {
if (!isPhoneNumberValid.value) return false return isPhoneNumberValid.value && verificationCode.value.length === 6
if (isPasswordLogin.value) {
return password.value.length >= 6
} else {
return verificationCode.value.length === 6
}
}) })
const hideRegister = computed(() => route.query.from === 'promotionInquire')
async function sendVerificationCode() { async function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return if (isCountingDown.value || !isPhoneNumberValid.value) return
@@ -41,6 +33,7 @@ async function sendVerificationCode() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return return
} }
const { data, error } = await useApiFetch('auth/sendSms') const { data, error } = await useApiFetch('auth/sendSms')
.post({ mobile: phoneNumber.value, actionType: 'login' }) .post({ mobile: phoneNumber.value, actionType: 'login' })
.json() .json()
@@ -80,59 +73,43 @@ async function handleLogin() {
showToast({ message: "请输入有效的手机号" }); showToast({ message: "请输入有效的手机号" });
return return
} }
if (isPasswordLogin.value) {
if (password.value.length < 6) {
showToast({ message: "密码长度不能小于6位" });
return
}
} else {
if (verificationCode.value.length !== 6) { if (verificationCode.value.length !== 6) {
showToast({ message: "请输入有效的验证码" }); showToast({ message: "请输入有效的验证码" });
return return
} }
}
if (!isAgreed.value) {
showToast({ message: "请先同意用户协议" });
return
}
// 显示验证组件
showCaptcha.value = true
}
// 验证成功回调 try {
function handleCaptchaSuccess() { const { data, error } = await mobileCodeLogin({
captchaVerified.value = true mobile: phoneNumber.value,
showCaptcha.value = false code: verificationCode.value
// 执行实际的登录逻辑 })
performLogin()
}
// 验证关闭回调
function handleCaptchaClose() {
showCaptcha.value = false
}
// 执行实际的登录逻辑
async function performLogin() {
const { data, error } = await useApiFetch('/user/mobileCodeLogin')
.post({ mobile: phoneNumber.value, code: verificationCode.value })
.json()
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
// 保存token
localStorage.setItem('token', data.value.data.accessToken) localStorage.setItem('token', data.value.data.accessToken)
localStorage.setItem('refreshAfter', data.value.data.refreshAfter) localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
localStorage.setItem('accessExpire', data.value.data.accessExpire) localStorage.setItem('accessExpire', data.value.data.accessExpire)
window.location.href = '/'
}
}
}
function toUserAgreement() {
router.push(`/userAgreement`)
}
function toPrivacyPolicy() { // 获取用户信息和代理信息
router.push(`/privacyPolicy`) await Promise.all([
userStore.fetchUserInfo(),
agentStore.fetchAgentStatus()
])
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: "登录失败,请重试" });
}
} }
onUnmounted(() => { onUnmounted(() => {
@@ -140,14 +117,19 @@ onUnmounted(() => {
clearInterval(timer) clearInterval(timer)
} }
}) })
const onClickLeft = () => { const onClickLeft = () => {
router.replace('/') router.back()
}
const goToRegister = () => {
router.push('/register')
} }
</script> </script>
<template> <template>
<div class="login-layout "> <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="login px-4 relative z-10">
<div class="mb-8 pt-20 text-left"> <div class="mb-8 pt-20 text-left">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
@@ -177,32 +159,17 @@ const onClickLeft = () => {
</div> </div>
</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 class="login-btn" :class="{ 'disabled': !canLogin }" @click="handleLogin" :disabled="!canLogin">
登录
</button>
<!-- 注册按钮推广链接来源的登录不展示 -->
<button v-if="!hideRegister" class="register-btn" @click="goToRegister">
注册成为代理
</button> </button>
</div> </div>
</div> </div>
<!-- 点击验证组件 -->
<ClickCaptcha :visible="showCaptcha" @success="handleCaptchaSuccess" @close="handleCaptchaClose" />
</div> </div>
</template> </template>
@@ -217,8 +184,6 @@ const onClickLeft = () => {
overflow: hidden; overflow: hidden;
} }
.login {}
/* 登录表单 */ /* 登录表单 */
.login-form { .login-form {
background-color: var(--color-bg-primary); background-color: var(--color-bg-primary);
@@ -295,37 +260,25 @@ const onClickLeft = () => {
cursor: not-allowed; cursor: not-allowed;
} }
/* 协议同意 */ /* 注册按钮 */
.agreement-wrapper { .register-btn {
display: flex; width: 100%;
align-items: center; padding: 0.875rem;
margin-top: 1.5rem; background-color: transparent;
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); color: var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer; cursor: pointer;
text-decoration: none; transition: all 0.3s;
letter-spacing: 0.25rem;
margin-top: 1rem;
} }
/* 提示文字 */ .register-btn:hover {
.notice-text { background-color: var(--color-primary);
font-size: 0.6875rem; color: var(--color-text-white);
color: var(--color-text-tertiary);
line-height: 1.5;
margin-bottom: 2rem;
} }
/* 登录按钮 */ /* 登录按钮 */

View File

@@ -1,31 +1,35 @@
<template> <template>
<div class="box-border min-h-screen"> <div class="box-border min-h-screen">
<div class="flex flex-col p-4 space-y-6"> <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"> @click="!isLoggedIn ? redirectToLogin() : null">
<div class="rounded-xl bg-white p-6">
<!-- 上半部分用户信息 -->
<div class="flex items-center gap-4 mb-4">
<div class="relative"> <div class="relative">
<!-- 头像容器添加overflow-hidden解决边框问题 --> <!-- 头像容器添加overflow-hidden解决边框问题 -->
<div class="overflow-hidden rounded-full p-0.5" :class="levelGradient.border"> <div class="overflow-hidden rounded-full p-0.5" :class="levelGradient.border">
<img :src="userAvatar || getDefaultAvatar()" alt="User Avatar" <img :src="userAvatar || getDefaultAvatar()" alt="User Avatar"
class="h-24 w-24 rounded-full border-4 border-white" /> class="h-20 w-20 rounded-full border-4 border-white" />
</div> </div>
<!-- 代理标识 --> <!-- 代理标识 -->
<div v-if="isAgent" class="absolute -bottom-2 -right-2"> <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" <div class="flex items-center justify-center rounded-full px-3 py-1 text-xs font-bold text-white shadow-sm"
:class="levelGradient.badge"> :class="levelGradient.badge">
{{ levelNames[level] }} {{ levelNamesMap[level] || levelNamesMap[1] }}
</div> </div>
</div> </div>
</div> </div>
<div class="space-y-1"> <div class="flex-1 space-y-1">
<h2 class="text-2xl font-bold" style="color: var(--van-text-color);"> <h2 class="text-2xl font-bold" style="color: var(--van-text-color);">
{{ {{
!isLoggedIn !isLoggedIn
? "点击登录" ? "点击登录"
: mobile : mobile
? maskName(mobile) ? mobile
: isWeChat : isWeChat
? "微信用户" ? "微信用户"
: "未绑定手机号" : "未绑定手机号"
@@ -33,189 +37,290 @@
</h2> </h2>
<!-- 手机号绑定提示 --> <!-- 手机号绑定提示 -->
<template v-if="isLoggedIn && !mobile"> <template v-if="isLoggedIn && !mobile">
<p @click.stop="showBindPhoneDialog" class="text-sm cursor-pointer hover:underline" <p @click.stop="showRegisterAgentDialog" class="text-sm cursor-pointer hover:underline"
style="color: var(--van-theme-primary);"> style="color: var(--van-theme-primary);">
点击绑定手机号码 点击注册成为代理
</p> </p>
</template> </template>
<p v-if="isAgent" class="text-sm font-medium" :class="levelGradient.text"> <!-- 普通用户申请成为代理提示 -->
🎖 {{ levelText[level] }} <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> </p>
</div> </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>
<VipBanner v-if="isAgent && (level === 'normal' || level === '')" /> </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>
</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>
<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>
<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>
<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>
</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>
<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>
<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>
<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>
</div>
</div>
</div>
<!-- 功能菜单 --> <!-- 功能菜单 -->
<div class=""> <div class="">
<div class="bg-white rounded-xl shadow-sm overflow-hidden"> <div class="bg-white rounded-xl shadow-sm p-4">
<template v-if="isAgent && ['VIP', 'SVIP'].includes(level)"> <div class="grid grid-cols-4 gap-4">
<button <!-- 升级功能入口如果不是钻石代理 -->
class="w-full flex items-center justify-between px-6 py-4 hover:bg-purple-50 transition-colors border-b border-gray-100" <button v-if="isAgent && level !== 3"
@click="toVipConfig"> class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-purple-50 transition-colors"
<div class="flex items-center gap-3"> @click="toUpgrade">
<img src="@/assets/images/me/dlbgpz.png" class="w-6 h-6 object-contain" alt="代理报告配置" /> <img src="@/assets/images/me/sjdl.svg" class="w-8 h-8 object-contain" alt="升级代理" />
<span class="text-purple-700 font-medium">代理报告配置</span> <span class="text-xs text-gray-700 font-medium text-center">升级代理</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </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" <button v-if="isAgent && level === 3"
@click="toVipRenewal"> class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-purple-50 transition-colors"
<div class="flex items-center gap-3"> @click="toUpgradeSubordinate">
<img src="@/assets/images/me/xfhy.png" class="w-6 h-6 object-contain" alt="代理会员" /> <img src="@/assets/images/me/sjxj.svg" class="w-8 h-8 object-contain" alt="调整下级级别" />
<div class="flex flex-col items-start"> <span class="text-xs text-gray-700 font-medium text-center">调整级别</span>
<span class="text-amber-700 font-medium">续费代理会员</span>
<span class="text-xs text-gray-500">有效期至 {{ formatExpiryTime(ExpiryTime) }}</span>
</div>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </button>
</template> <!-- 邀请码管理入口 -->
<button <button v-if="isAgent"
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors border-b border-gray-100" class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toHistory"> @click="toInviteCodeManage">
<div class="flex items-center gap-3"> <img src="@/assets/images/me/yqmgl.svg" class="w-8 h-8 object-contain" alt="邀请码管理" />
<img src="@/assets/images/me/wdbg.png" class="w-6 h-6 object-contain" alt="我的报告" /> <span class="text-xs text-gray-700 font-medium text-center">邀请码管理</span>
<span class="text-gray-700 font-medium">我的报告</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </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" <button v-if="isAgent"
@click="toApiStore"> class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-green-50 transition-colors"
<div class="flex items-center gap-3"> @click="toRealNameAuth">
<img src="@/assets/images/me/apisd.png" class="w-6 h-6 object-contain" alt="API商店" /> <img src="@/assets/images/me/smrz.svg" class="w-8 h-8 object-contain" alt="提现" />
<span class="text-gray-700 font-medium">API商店</span> <span class="text-xs text-gray-700 font-medium text-center">实名认证</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </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" <!-- <button v-if="isAgent"
@click="toBigData"> class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
<div class="flex items-center gap-3"> @click="toWithdraw">
<img src="@/assets/images/me/qyfkyj.png" class="w-6 h-6 object-contain" alt="企业风控预警" /> <img src="@/assets/images/me/tx.svg" class="w-8 h-8 object-contain" alt="提现" />
<span class="text-gray-700 font-medium">企业风控预警</span> <span class="text-xs text-gray-700 font-medium text-center">提现</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> --> </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" <button v-if="isAgent"
@click="toCooperation"> class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
<div class="flex items-center gap-3"> @click="toAgentReport">
<img src="@/assets/images/me/swhz.png" class="w-6 h-6 object-contain" alt="商务合作" /> <img src="@/assets/images/me/tgcxjl.svg" class="w-8 h-8 object-contain" alt="推广查询记录" />
<span class="text-gray-700 font-medium">商务合作</span> <span class="text-xs text-gray-700 font-medium text-center">推广查询记录</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </button>
<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" class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toUserAgreement"> @click="toUserAgreement">
<div class="flex items-center gap-3"> <img src="@/assets/images/me/yhxy.svg" class="w-8 h-8 object-contain" alt="用户协议" />
<img src="@/assets/images/me/yhxy.png" class="w-6 h-6 object-contain" alt="用户协议" /> <span class="text-xs text-gray-700 font-medium text-center">用户协议</span>
<span class="text-gray-700 font-medium">用户协议</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </button>
<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" class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toPrivacyPolicy"> @click="toPrivacyPolicy">
<div class="flex items-center gap-3"> <img src="@/assets/images/me/yszc.svg" class="w-8 h-8 object-contain" alt="隐私政策" />
<img src="@/assets/images/me/yszc.png" class="w-6 h-6 object-contain" alt="隐私政策" /> <span class="text-xs text-gray-700 font-medium text-center">隐私政策</span>
<span class="text-gray-700 font-medium">隐私政策</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </button>
<button <button
class="w-full flex items-center justify-between px-6 py-4 hover:bg-blue-50 transition-colors" class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-blue-50 transition-colors"
@click="toService"> @click="toService">
<div class="flex items-center gap-3"> <img src="@/assets/images/me/lxkf.svg" class="w-8 h-8 object-contain" alt="联系客服" />
<img src="@/assets/images/me/lxkf.png" class="w-6 h-6 object-contain" alt="联系客服" /> <span class="text-xs text-gray-700 font-medium text-center">联系客服</span>
<span class="text-gray-700 font-medium">联系客服</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </button>
<button v-if="isLoggedIn && !isWeChat" <button v-if="isLoggedIn && !isWeChat"
class="w-full flex items-center justify-between px-6 py-4 hover:bg-red-50 transition-colors" class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-red-50 transition-colors"
@click="handleLogout"> @click="handleLogout">
<div class="flex items-center gap-3"> <img src="@/assets/images/me/tcdl.png" class="w-8 h-8 object-contain" alt="退出登录" />
<img src="@/assets/images/me/tcdl.png" class="w-6 h-6 object-contain" alt="退出登录" /> <span class="text-xs text-gray-700 font-medium text-center">退出登录</span>
<span class="text-gray-700 font-medium">退出登录</span>
</div>
<img src="@/assets/images/me/right.png" class="w-4 h-4" alt="右箭头" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<BindPhoneDialog />
</div>
</template> </template>
<script setup> <script setup>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { ref, computed, onBeforeMount } from "vue"; import { ref, computed, onBeforeMount, onMounted } from "vue";
const router = useRouter(); import { useRouter } from "vue-router";
import headShot from "@/assets/images/head_shot.webp"; import headShot from "@/assets/images/head_shot.webp";
import { useAgentStore } from "@/stores/agentStore"; import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore"; import { useUserStore } from "@/stores/userStore";
import { useEnv } from "@/composables/useEnv"; import { useEnv } from "@/composables/useEnv";
import { useDialogStore } from "@/stores/dialogStore"; import { useDialogStore } from "@/stores/dialogStore";
import useApiFetch from "@/composables/useApiFetch"; import useApiFetch from "@/composables/useApiFetch";
import { getRevenueInfo } from '@/api/agent';
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
const router = useRouter();
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const userStore = useUserStore(); const userStore = useUserStore();
const dialogStore = useDialogStore(); const dialogStore = useDialogStore();
const { isAgent, level, ExpiryTime } = storeToRefs(agentStore); const { isAgent, level, levelName, agentCode } = storeToRefs(agentStore);
const { userName, userAvatar, isLoggedIn, mobile } = storeToRefs(userStore); const { userName, userAvatar, isLoggedIn, mobile } = storeToRefs(userStore);
const { isWeChat } = useEnv(); const { isWeChat } = useEnv();
const revenueData = ref(null);
const levelNames = { // 等级名称映射(数字等级)
normal: "普通代理", const levelNamesMap = {
"": "普通代理", 1: "普通代理",
VIP: "VIP代理", 2: "黄金代理",
SVIP: "SVIP代理", 3: "钻石代理",
}; };
const levelText = { const levelTextMap = {
normal: "基础代理特权", 1: "基础代理特权",
"": "基础代理特权", 2: "高级代理特权",
VIP: "高级代理特权", 3: "尊享代理特权",
SVIP: "尊享代理特权",
}; };
const levelGradient = computed(() => ({ const levelGradient = computed(() => {
border: { const currentLevel = level.value || 1;
normal: "bg-gradient-to-r from-gray-300 to-gray-400", const gradients = {
"": "bg-gradient-to-r from-gray-300 to-gray-400", 1: {
VIP: "bg-gradient-to-r from-yellow-400 to-amber-500", border: "bg-gradient-to-r from-gray-300 to-gray-400",
SVIP: "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-gray-500 to-gray-600",
}[level.value], text: "text-gray-600",
cardBorder: "bg-gradient-to-r from-gray-300 to-gray-400",
badge: { },
normal: "bg-gradient-to-r from-gray-500 to-gray-600", 2: {
"": "bg-gradient-to-r from-gray-500 to-gray-600", border: "bg-gradient-to-r from-yellow-400 to-amber-500",
VIP: "bg-gradient-to-r from-yellow-500 to-amber-600", badge: "bg-gradient-to-r from-yellow-500 to-amber-600",
SVIP: "bg-gradient-to-r from-purple-500 to-pink-500", text: "text-amber-600",
}[level.value], cardBorder: "bg-gradient-to-r from-yellow-400 to-amber-500",
},
text: { 3: {
normal: "text-gray-600", border: "bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_15px_rgba(163,51,200,0.2)]",
"": "text-gray-600", badge: "bg-gradient-to-r from-purple-500 to-pink-500",
VIP: "text-amber-600", text: "text-purple-600",
SVIP: "text-purple-600", cardBorder: "bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_20px_rgba(163,51,200,0.3)]",
}[level.value], },
})); };
return gradients[currentLevel] || gradients[1];
});
const maskName = (name) => { const maskName = (name) => {
if (!name || name.length < 11) return name; if (!name || name.length < 11) return name;
return name.substring(0, 3) + "****" + name.substring(7); 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() { function toUserAgreement() {
router.push(`/userAgreement`); router.push(`/userAgreement`);
} }
@@ -266,42 +371,73 @@ function handleCancelAccount() {
function toService() { function toService() {
window.location.href = window.location.href =
"https://work.weixin.qq.com/kfid/kfc8a32720024833f57"; // 跳转到客服页面 "https://work.weixin.qq.com/kfid/kfc82d4424e4b19e5f3"; // 跳转到客服页面
} }
const toVipConfig = () => { const toUpgrade = () => {
router.push({ name: "agentVipConfig" }); router.push({ name: "agentUpgrade" });
}; };
const toVipRenewal = () => { const toUpgradeSubordinate = () => {
router.push(`/agent/vipApply`); router.push({ name: "upgradeSubordinate" });
}; };
function formatExpiryTime(expiryTimeStr) { const toInviteCodeManage = () => {
if (!expiryTimeStr) return "未知"; router.push({ name: "invitation" });
// 假设expiryTimeStr格式是 "YYYY-MM-DD HH:MM:SS" };
// 只返回日期部分 "YYYY-MM-DD"
return expiryTimeStr.split(" ")[0]; 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 = () => { const getDefaultAvatar = () => {
if (!isAgent.value) return headShot; if (!isAgent.value) return headShot;
switch (level.value) { const currentLevel = level.value || 1;
case "normal": switch (currentLevel) {
case "": case 1:
return "/image/shot_nonal.png"; return "/image/shot_nonal.png";
case "VIP": case 2:
return "/image/shot_vip.png"; return "/image/shot_vip.png";
case "SVIP": case 3:
return "/image/shot_svip.png"; return "/image/shot_svip.png";
default: default:
return headShot; return headShot;
} }
}; };
const showBindPhoneDialog = () => { const showRegisterAgentDialog = () => {
dialogStore.openBindPhone(); dialogStore.openRegisterAgent();
};
const toRegister = () => {
router.push("/register");
}; };
onBeforeMount(() => { onBeforeMount(() => {
@@ -326,6 +462,12 @@ onBeforeMount(() => {
} }
} }
}); });
onMounted(() => {
if (isAgent.value) {
getRevenueData();
}
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -29,19 +29,20 @@
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-600">支付类型</span> <span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{ <span class="text-gray-800">{{
paymentType === "agent_vip" paymentType === "agent_upgrade"
? "代理会员" ? "代理升级"
: "查询服务" : "查询服务"
}}</span> }}</span>
</div> </div>
</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"> <div class="action-buttons grid grid-cols-1 gap-4">
<van-button block type="primary" class="rounded-lg" @click="handleNavigation"> <van-button block type="primary" class="rounded-lg" @click="handleNavigation">
{{ {{
paymentType === "agent_vip" paymentType === "agent_upgrade"
? "查看会员权益" ? "查看代理信息"
: "查看查询结果" : "查看查询结果"
}} }}
</van-button> </van-button>
@@ -71,8 +72,8 @@
<div class="flex justify-between mb-4"> <div class="flex justify-between mb-4">
<span class="text-gray-600">支付类型</span> <span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{ <span class="text-gray-800">{{
paymentType === "agent_vip" paymentType === "agent_upgrade"
? "代理会员" ? "代理升级"
: "查询服务" : "查询服务"
}}</span> }}</span>
</div> </div>
@@ -94,7 +95,7 @@
</p> </p>
</div> </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 block type="primary" class="rounded-lg" @click="contactService">
联系客服 联系客服
</van-button> </van-button>
@@ -134,8 +135,8 @@
<div v-if="!isApiError" class="flex justify-between mb-4"> <div v-if="!isApiError" class="flex justify-between mb-4">
<span class="text-gray-600">支付类型</span> <span class="text-gray-600">支付类型</span>
<span class="text-gray-800">{{ <span class="text-gray-800">{{
paymentType === "agent_vip" paymentType === "agent_upgrade"
? "代理会员" ? "代理升级"
: "查询服务" : "查询服务"
}}</span> }}</span>
</div> </div>
@@ -408,8 +409,8 @@ onBeforeUnmount(() => {
// 处理导航逻辑 // 处理导航逻辑
function handleNavigation() { function handleNavigation() {
if (paymentType.value === "agent_vip") { if (paymentType.value === "agent_upgrade") {
// 跳转到代理会员页面 // 跳转到代理主页
router.replace("/agent"); router.replace("/agent");
agentStore.fetchAgentStatus(); agentStore.fetchAgentStatus();
userStore.fetchUserInfo(); userStore.fetchUserInfo();
@@ -431,7 +432,7 @@ function goHome() {
function contactService() { function contactService() {
// 可以替换为实际的客服联系逻辑,如打开聊天窗口或跳转到客服页面 // 可以替换为实际的客服联系逻辑,如打开聊天窗口或跳转到客服页面
window.location.href = 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>
<div class="leading-relaxed"> <div class="leading-relaxed">
我们深知个人信息对您的重要性我们将按法律法规要求采取相应安全保护措施尽力保护您的个人信息安全可控 我们深知个人信息对您的重要性我们将按法律法规要求采取相应安全保护措施尽力保护您的个人信息安全可控
有鉴于此海南省学宇思网络科技有限公司以下简称我们一查查作为一查查产品及服务的提供者制定本隐私政策下称本政策并提醒您 有鉴于此海南海宇大数据有限公司以下简称我们一查查作为一查查产品及服务的提供者制定本隐私政策下称本政策并提醒您
</div> </div>
<div class="leading-relaxed"> <div class="leading-relaxed">
本政策适用于全部一查查产品及服务如我们关联公司的产品或服务中使用了一查查提供的产品或服务但未设独立的隐私政策的 本政策适用于全部一查查产品及服务如我们关联公司的产品或服务中使用了一查查提供的产品或服务但未设独立的隐私政策的
@@ -35,7 +35,7 @@
<!-- 第一部分 --> <!-- 第一部分 -->
<div class="leading-relaxed"> <div class="leading-relaxed">
<div> <div>
1一查查服务提供者是指研发并提供一查查产品和服务法律主体海南省学宇思网络科技有限公司下称我们一查查 1一查查服务提供者是指研发并提供一查查产品和服务法律主体海南海宇大数据有限公司下称我们一查查
</div> </div>
<div> <div>
2一查查用户是指注册一查查账户的用户以下称 2一查查用户是指注册一查查账户的用户以下称
@@ -440,7 +440,7 @@
<div class="leading-relaxed"> <div class="leading-relaxed">
隐私政策的解释及争议解决均应适用中华人民共和国大陆地区法律 隐私政策的解释及争议解决均应适用中华人民共和国大陆地区法律
与本隐私政策相关的任何纠纷双方应协商友好解决若不能协商解决 与本隐私政策相关的任何纠纷双方应协商友好解决若不能协商解决
应将争议提交至海南省学宇思网络科技有限公司注册地有管辖权的人民法院解决 应将争议提交至海南海宇大数据有限公司注册地有管辖权的人民法院解决
</div> </div>
<div class="leading-relaxed"> <div class="leading-relaxed">
隐私政策的标题仅为方便及阅读而设并不影响正文其中任何规定的含义或解释 隐私政策的标题仅为方便及阅读而设并不影响正文其中任何规定的含义或解释

View File

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

View File

@@ -1,28 +1,34 @@
<script setup> <script setup>
const router = useRouter(); const router = useRouter();
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import SectionTitle from "@/components/SectionTitle.vue";
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore); const { isAgent, isDiamond } = storeToRefs(agentStore);
const userStore = useUserStore(); const userStore = useUserStore();
const { isLoggedIn } = storeToRefs(userStore); 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 bannerImg from "@/assets/images/index/n/01.jpg";
import marriageRiskIcon from "@/assets/images/promote/marriage_risk_bg.png"; import productRecommendImg from "@/assets/images/index/n/02.png";
import housekeepingRiskIcon from "@/assets/images/promote/housekeeping_risk_bg.png"; import inviteSubordinateImg from "@/assets/images/index/n/03.png";
import backgroundcheckIcon from "@/assets/images/promote/backgroundcheck_bg.png"; import myTeamImg from "@/assets/images/index/n/04.png";
import consumerFinanceReportIcon from "@/assets/images/promote/consumer_finance_report_bg.png"; import personalDataImg from "@/assets/images/index/n/05.png";
import banner1 from "@/assets/images/promote/banner_1.png"; import companyDataImg from "@/assets/images/index/n/07.png";
import banner2 from "@/assets/images/promote/banner_2.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) { function toInquire(name) {
// 跳转到推广页面,传递 feature 参数 // 跳转到推广页面,传递 feature 参数
router.push({ name: "agentPromote", query: { feature: name } }); router.push({ name: "agentPromote", query: { feature: name } });
} }
function toInvitation() { function toInvitation() {
router.push({ name: "invitation" }); router.push({ name: "invitation" });
} }
const toPromote = () => { const toPromote = () => {
router.push({ name: "agentPromote" }); router.push({ name: "agentPromote" });
}; };
@@ -47,302 +53,285 @@ const toWithdraw = () => {
router.push({ name: "withdraw" }); router.push({ name: "withdraw" });
}; };
const toSubordinateList = () => { const toTeamList = () => {
router.push({ name: "subordinateList" }); router.push({ name: "teamList" });
};
// 个人大数据查询
const toPersonalDataQuery = () => {
router.push({ name: "agentPromote", query: { feature: "riskassessment" } });
}; };
const services = ref([ // 产品UI配置映射根据 product_en 匹配)
{ const productUIConfig = {
'riskassessment': {
title: "个人大数据", title: "个人大数据",
name: "personalData", subtitle: "个人信用 精准查询",
subtitle: "数据洞察,规避风险", bg: personalDataImg,
bg: personalDataIcon,
goColor: "#6699ff", goColor: "#6699ff",
productId: 27, isHighlight: true, // 重点推荐
costPrice: null,
}, },
{ 'marriage': {
title: "婚恋风险", title: "情侣报告",
name: "marriage", subtitle: "相信才能相依 相爱才能永久",
subtitle: "深度了解,坦诚相爱", bg: marriageImg,
bg: marriageRiskIcon,
goColor: "#ff99cc", goColor: "#ff99cc",
productId: 4,
costPrice: null,
}, },
{ 'backgroundcheck': {
title: "入职背调", title: "入职背调",
name: "backgroundcheck", subtitle: "查询便可 慧眼识英雄",
subtitle: "人才甄选,用人无忧", bg: backgroundCheckImg,
bg: backgroundcheckIcon,
goColor: "#7db3ff", goColor: "#7db3ff",
productId: 1,
costPrice: null,
}, },
{ 'companyinfo': {
title: "小微企业", title: "企业大数据",
name: "companyinfo", subtitle: "信任是合作 永恒的基石",
subtitle: "风险监控,稳健经营", bg: companyDataImg,
bg: companyIcon,
goColor: "#ffaa66", goColor: "#ffaa66",
productId: 2,
costPrice: null,
}, },
{ 'homeservice': {
title: "家政风险", title: "家政报告",
name: "homeservice", subtitle: "口碑与能力 一查便知",
subtitle: "身份核验,守护家庭", bg: housekeepingImg,
bg: housekeepingRiskIcon,
goColor: "#66cccc", goColor: "#66cccc",
productId: 3,
costPrice: null,
}, },
{ 'consumerFinanceReport': {
title: "消金报告", title: "消金报告",
name: "consumerFinanceReport", subtitle: "",
subtitle: "深度分析,智能风控", bg: consumerFinanceImg,
bg: consumerFinanceReportIcon, goColor: "#a259ff",
goColor: "#ffaa66",
productId: 28, // 使用贷前风险的id因为可能是同一个产品
costPrice: null,
}, },
// {
// 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 () => { const personalDataService = ref({
try { ...productUIConfig['riskassessment'],
const { data, error } = await useApiFetch("/agent/product_config") name: "riskassessment",
.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 noticeText = ref([]); // 其他服务列表(排除个人大数据,默认显示所有产品)
const toCooperation = () => { const otherServices = computed(() => {
window.location.href = "https://www.tianyuandata.com"; return Object.keys(productUIConfig)
}; .filter(key => key !== 'riskassessment')
const toBigData = () => { .map(key => ({
window.location.href = "https://www.tybigdata.com/"; ...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> </script>
<template> <template>
<div class="box-border from-blue-100 to-white bg-gradient-to-b"> <div class="box-border bg-white min-h-screen">
<div class="relative"> <!-- 顶部横幅 -->
<van-swipe :autoplay="3000" indicator-color="white"> <div class="relative w-full" @click="toPersonalDataQuery">
<van-swipe-item v-for="(banner, index) in bannerImages" :key="index" @click="toPromote"> <img :src="bannerImg" class="w-full" alt="一站式大数据 风险评估平台" />
<img class="h-full w-full" :src="banner" />
</van-swipe-item>
</van-swipe>
</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>
<div class="text-center flex flex-col justify-center items-center" @click="toInvitation"> <!-- 服务类别网格 -->
<div <div class="px-4 py-4">
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="grid grid-cols-3 gap-3 mb-3">
</div> <div class="flex flex-col items-center justify-center" @click="toPromote">
<div class="text-center mt-1 font-bold text-sm">邀请下级</div> <img :src="productRecommendImg" class="w-20 h-20 rounded-lg shadow-sm object-cover" alt="产品推荐" />
</div> <div class="mt-2 text-center text-gray-700 text-sm font-bold">
产品推荐
<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>
<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>
<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>
<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>
<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>
<div class="flex items-center justify-between mx-4 mb-2"> <div class="flex flex-col items-center justify-center" @click="toTeamList">
<SectionTitle title="推广服务" /> <img :src="myTeamImg" class="w-20 h-20 rounded-lg shadow-sm object-cover" alt="我的团队" />
<div class="text-xs text-gray-500 flex items-center gap-1.5"> <div class="mt-2 text-center text-gray-700 text-sm font-bold">
<span class="opacity-80"></span> 我的团队
<span>滑动查看更多</span> </div>
<span class="opacity-80"></span>
</div> </div>
</div> </div>
<div class="relative p-4 pt-0"> <!-- 第二排和第三排左侧大卡片 + 右侧小卡片 -->
<div class="services-scroll-container"> <div class="grid grid-cols-2 gap-3">
<div class="services-scroll-wrapper"> <!-- 左侧个人大数据和情侣报告 -->
<template v-for="(service, index) in allServices" :key="index"> <div class="flex flex-col gap-3">
<div class="relative flex flex-col px-4 py-2 rounded-xl shadow service-card" <!-- 个人大数据重点推荐- 占2.5个高度 -->
:style="`background: url(${service.bg}) no-repeat; background-size: contain; background-position: center;`" <div class="relative flex-[2.5]" @click="toPersonalDataQuery">
@click="toInquire(service.name)"> <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="flex flex-col items-start flex-1">
<div class="mt-1 text-left text-gray-600 font-bold"> <div class="mt-1 max-w-max text-left text-gray-600 font-bold text-lg">
{{ service.title }} {{ personalDataService.title }}
</div> </div>
<div class="mt-2 rounded-lg px-2 py-1 text-xs text-white w-max flex items-center" <div class="text-left text-sm text-gray-500 leading-relaxed whitespace-pre-line"
:style="`background-color: ${service.goColor}`"> 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="右箭头" <img src="@/assets/images/index/go_icon.png" alt="右箭头"
class="ml-0.5 h-3 w-3 inline-block align-middle" /> class="ml-0.5 h-3 w-3 inline-block align-middle" />
</div> </div>
</div> </div>
<div </div>
class="absolute bottom-0 left-0 right-0 rounded-b-xl px-2 py-1 text-xs text-white text-center"> </template>
{{ getCostPriceText(service) }}
<!-- 家政报告 -->
<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>
</div> </div>
</template> </template>
</div> </div>
</div> </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> </div>
</template> </template>
<style scoped> <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 { div[class*="rounded-lg"]:active {
overflow-x: auto; transform: scale(0.98);
overflow-y: hidden; opacity: 0.9;
-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;
} }
</style> </style>

View File

@@ -26,17 +26,21 @@ function isFinishPayment() {
const query = new URLSearchParams(window.location.search); const query = new URLSearchParams(window.location.search);
let orderNo = query.get("out_trade_no"); let orderNo = query.get("out_trade_no");
if (orderNo) { if (orderNo) {
console.log("🎯 Detected payment result, navigating to report with orderNo:", orderNo);
// 延迟跳转,确保页面已完全加载
setTimeout(() => {
router.push({ path: "/report", query: { orderNo } }); router.push({ path: "/report", query: { orderNo } });
}, 100);
} }
} }
import { getLinkData } from "@/api/agent";
async function getProduct() { async function getProduct() {
linkIdentifier.value = route.params.linkIdentifier; linkIdentifier.value = route.params.linkIdentifier;
const { data: agentLinkData, error: agentLinkError } = await useApiFetch( const { data: agentLinkData, error: agentLinkError } = await getLinkData(
`/agent/link?link_identifier=${linkIdentifier.value}` linkIdentifier.value
) );
.get()
.json();
if (agentLinkData.value && !agentLinkError.value) { if (agentLinkData.value && !agentLinkError.value) {
if (agentLinkData.value.code === 200) { if (agentLinkData.value.code === 200) {
feature.value = agentLinkData.value.data.product_en; 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(() => { onMounted(() => {
(function (d, t) { (function (d, t) {
const BASE_URL = "https://service.quannengcha.com"; const BASE_URL = "https://service.onecha.cn";
const g = d.createElement(t), const g = d.createElement(t),
s = d.getElementsByTagName(t)[0]; s = d.getElementsByTagName(t)[0];
g.src = BASE_URL + "/packs/js/sdk.js"; g.src = BASE_URL + "/packs/js/sdk.js";

View File

@@ -9,14 +9,27 @@ const refreshing = ref(false)
const finished = ref(false) const finished = ref(false)
const page = ref(1) const page = ref(1)
const pageSize = 8 const pageSize = 8
const activeTab = ref('order') // 'order' 或 'invite'
// 获取收益列表 // 数据
const fetchRewardDetails = async () => { const userInfo = ref({})
if (loading.value || finished.value) return 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 loading.value = true
const tabType = activeTab.value
const { data, error } = await useApiFetch( 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() .get()
.json() .json()
@@ -30,201 +43,107 @@ const fetchRewardDetails = async () => {
level: data.value.data.level_name || '普通', level: data.value.data.level_name || '普通',
mobile: data.value.data.mobile, mobile: data.value.data.mobile,
} }
// 更新汇总数据 // 更新统计数据
summary.value = { orderStats.value = data.value.data.order_stats || {}
totalReward: data.value.data.total_earnings, rebateStats.value = data.value.data.rebate_stats || {}
totalContribution: data.value.data.total_contribution, inviteStats.value = data.value.data.invite_stats || {}
totalOrders: data.value.data.total_orders,
} // 清空列表
// 设置默认的统计类型 if (tabType === 'order') {
statistics.value = [ orderList.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
} else { } 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++
} }
finished.value = data.value.data.list.length < pageSize
} else { } else {
finished.value = true finished.value = true
} }
} else {
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 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 = () => { const onRefresh = () => {
finished.value = false finished.value = false
page.value = 1 page.value = 1
fetchRewardDetails().finally(() => { fetchDetail().finally(() => {
refreshing.value = false refreshing.value = false
}) })
} }
const rewardDetails = ref([]) // 获取等级标签样式
const userInfo = ref({}) const getLevelClass = (level) => {
const summary = ref({}) const levelNum = typeof level === 'number' ? level : parseInt(level)
const statistics = ref([]) switch (levelNum) {
case 3:
onMounted(() => { return 'bg-purple-100 text-purple-600'
fetchRewardDetails() case 2:
}) return 'bg-yellow-100 text-yellow-600'
case 1:
// 获取收益类型样式 default:
const getRewardTypeClass = type => { return 'bg-gray-100 text-gray-600'
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',
} }
return typeMap[type] || 'bg-gray-100 text-gray-600'
} }
// 获取收益类型图标 // 获取等级显示名称
const getRewardTypeIcon = type => { const getLevelName = (level) => {
const iconMap = { const levelNum = typeof level === 'number' ? level : parseInt(level)
descendant_promotion: 'gift', const levelMap = {
cost: 'gold-coin', 1: '普通',
pricing: 'balance-pay', 2: '黄金',
descendant_withdraw: 'cash-back-record', 3: '钻石'
descendant_upgrade_vip: 'fire',
descendant_upgrade_svip: 'fire',
descendant_new_active: 'medal',
descendant_stay_active: 'medal',
} }
return iconMap[type] || 'balance-o' return levelMap[levelNum] || '普通'
}
// 获取收益类型描述
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] || '未知类型'
} }
// 格式化时间 // 格式化时间
const formatTime = timeStr => { const formatTime = timeStr => {
if (!timeStr) return '-' if (!timeStr) return '-'
const date = new Date(timeStr) return timeStr.split(' ')[0]
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
} }
// 格式化金额 // 格式化金额
@@ -232,10 +151,14 @@ const formatNumber = num => {
if (!num) return '0.00' if (!num) return '0.00'
return Number(num).toFixed(2) return Number(num).toFixed(2)
} }
onMounted(() => {
fetchDetail()
})
</script> </script>
<template> <template>
<div class="reward-detail"> <div class="subordinate-detail">
<!-- 用户信息卡片 --> <!-- 用户信息卡片 -->
<div class="p-4"> <div class="p-4">
<div class="bg-white rounded-xl shadow-sm p-5 mb-4"> <div class="bg-white rounded-xl shadow-sm p-5 mb-4">
@@ -248,81 +171,88 @@ const formatNumber = num => {
</div> </div>
</div> </div>
<div class="text-sm text-gray-500 mb-4">成为下级代理时间{{ formatTime(userInfo.createTime) }}</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-center">
<div class="text-gray-500 text-sm mb-1">推广单量</div> <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-xl font-semibold text-blue-600">{{ orderStats.total_orders || 0 }}</div>
</div> </div>
<div class="text-center"> <div class="text-center">
<div class="text-gray-500 text-sm mb-1">总收益</div> <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-xl font-semibold text-green-600">{{ orderStats.month_orders || 0 }}</div>
</div> </div>
<div class="text-center"> <div class="text-center">
<div class="text-gray-500 text-sm mb-1">总贡献</div> <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-xl font-semibold text-orange-600">{{ orderStats.today_orders || 0 }}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 贡献统计卡片 --> <!-- 返佣统计卡片 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4"> <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="text-base font-medium text-gray-800 mb-3">返佣统计</div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-3 gap-3">
<div <div class="text-center">
v-for="item in statistics" <div class="text-gray-500 text-sm mb-1">总返佣金额</div>
:key="item.type" <div class="text-xl font-semibold text-blue-600">¥{{ formatNumber(rebateStats.total_rebate_amount) }}</div>
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>
<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> </div>
</div> </div>
<!-- 邀请统计卡片 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4"> <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="text-base font-medium text-gray-800">贡献记录</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-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchDetail">
v-model:loading="loading" <div class="p-2">
:finished="finished" <div v-if="orderList.length === 0" class="text-center text-gray-500 py-8">暂无订单记录</div>
finished-text="没有更多了" <div v-else v-for="item in orderList" :key="item.order_no"
@load="fetchRewardDetails" class="order-item mb-3 border-b border-gray-200 pb-3">
>
<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 justify-between">
<div class="flex items-center space-x-3"> <div class="flex-1">
<van-icon <div class="font-medium text-gray-800 mb-1">{{ item.product_name || '未知产品' }}</div>
:name="getRewardTypeIcon(item.type)" <div class="text-xs text-gray-500 mb-1">订单号{{ item.order_no }}</div>
class="text-lg"
:class="getRewardTypeClass(item.type).split(' ')[1]"
/>
<div>
<div class="font-medium text-gray-800">{{ getRewardTypeDescription(item.type) }}</div>
<div class="text-xs text-gray-500">{{ formatTime(item.create_time) }}</div> <div class="text-xs text-gray-500">{{ formatTime(item.create_time) }}</div>
</div> </div>
<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>
<div class="text-right"> <div class="text-sm text-gray-500 mb-1">返佣金额</div>
<div class="text-base font-semibold" :class="getRewardTypeClass(item.type).split(' ')[1]"> <div class="text-base font-semibold text-green-600">¥{{ formatNumber(item.rebate_amount) }}
¥{{ formatNumber(item.amount) }}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -330,22 +260,47 @@ const formatNumber = num => {
</div> </div>
</van-list> </van-list>
</van-pull-refresh> </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> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.reward-detail { .subordinate-detail {
min-height: 100vh; min-height: 100vh;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
.reward-item { .order-item,
.invite-item {
transition: transform 0.2s; transition: transform 0.2s;
} }
.reward-item:active { .order-item:active,
.invite-item:active {
transform: scale(0.98); transform: scale(0.98);
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useAgentStore } from '@/stores/agentStore' import { useAgentStore } from '@/stores/agentStore'
import useApiFetch from '@/composables/useApiFetch' import { getSubordinateList } from '@/api/agent'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const agentStore = useAgentStore() const agentStore = useAgentStore()
@@ -13,8 +13,17 @@ const pageSize = 8
const refreshing = ref(false) const refreshing = ref(false)
const router = useRouter() const router = useRouter()
// 加载更多数据
const onLoad = async () => {
if (!finished.value) {
await fetchSubordinates()
}
}
onBeforeMount(() => { onBeforeMount(() => {
fetchSubordinates() // 初始化时重置状态
page.value = 1
finished.value = false
}) })
// 计算统计数据 // 计算统计数据
const statistics = ref({ const statistics = ref({
@@ -26,21 +35,46 @@ const fetchSubordinates = async () => {
if (loading.value || finished.value) return if (loading.value || finished.value) return
loading.value = true loading.value = true
const { data, error } = await useApiFetch(`/agent/subordinate/list?page=${page.value}&page_size=${pageSize}`) try {
.get() const { data, error } = await getSubordinateList({
.json() page: page.value,
page_size: pageSize
})
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
statistics.value.totalSubordinates = data.value.data.total statistics.value.totalSubordinates = data.value.data.total
if (page.value === 1) { if (page.value === 1) {
subordinates.value = data.value.data.list subordinates.value = data.value.data.list || []
} else { } else {
subordinates.value.push(...data.value.data.list) subordinates.value.push(...(data.value.data.list || []))
}
}
} }
// 判断是否加载完成
if (data.value.data.list && data.value.data.list.length < pageSize) {
finished.value = true finished.value = true
} else if (subordinates.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 loading.value = false
}
} }
// 下拉刷新 // 下拉刷新
@@ -64,23 +98,37 @@ const formatNumber = num => {
return Number(num).toFixed(2) return Number(num).toFixed(2)
} }
// 获取等级标签样式 // 获取等级标签样式新系统1=普通2=黄金3=钻石)
const getLevelClass = level => { const getLevelClass = (level) => {
switch (level) { // level可能是数字或字符串
case 'SVIP': const levelNum = typeof level === 'number' ? level : parseInt(level)
switch (levelNum) {
case 3:
return 'bg-purple-100 text-purple-600' return 'bg-purple-100 text-purple-600'
case 'VIP': case 2:
return 'bg-blue-100 text-blue-600' return 'bg-yellow-100 text-yellow-600'
case 1:
default: default:
return 'bg-gray-100 text-gray-600' 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 => { const viewDetail = item => {
router.push({ router.push({
name: 'subordinateDetail', name: 'subordinateDetail',
params: { id: item.id }, params: { id: item.agent_id || item.id },
}) })
} }
@@ -104,7 +152,7 @@ onMounted(() => {
</div> </div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <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 class="p-4">
<div v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item"> <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"> <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 justify-between mb-4">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div <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 }} {{ index + 1 }}
</div> </div>
<div class="text-xl font-semibold text-gray-800">{{ item.mobile }}</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)]"> <span
{{ item.level ? item.level : '普通' }}代理 :class="['px-3 py-1 rounded-full text-sm font-medium', getLevelClass(item.level || item.level_name)]">
{{ getLevelName(item.level || item.level_name) }}代理
</span> </span>
</div> </div>
</div> </div>
@@ -127,27 +175,21 @@ onMounted(() => {
<div class="text-sm text-gray-500 mb-5">成为下级代理时间{{ item.create_time }}</div> <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-center">
<div class="text-gray-500 text-sm mb-1">推广单量</div> <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-xl font-semibold text-blue-600">{{ item.total_orders || 0 }}</div>
</div> </div>
<div class="text-center"> <div class="text-center">
<div class="text-gray-500 text-sm mb-1">收益</div> <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 class="text-xl font-semibold text-green-600">¥{{ formatNumber(item.total_amount) }}</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> </div>
</div> </div>
<!-- 查看详情按钮 --> <!-- 查看详情按钮 -->
<div class="flex justify-end"> <div class="flex justify-end">
<button <button @click="viewDetail(item)"
@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">
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" /> <van-icon name="eye" class="mr-1.5" />
查看详情 查看详情
</button> </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">{{ formatCount(item.today_invites || 0) }}</div>
</div>
<div class="member-stat-item">
<div class="member-stat-label">本月邀请</div>
<div class="member-stat-value">{{ formatCount(item.month_invites || 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.today_queries || 0) }}</div>
</div>
<div class="member-stat-item">
<div class="member-stat-label">本月查询</div>
<div class="member-stat-value">{{ formatCount(item.month_queries || 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>
<!-- 查看详情按钮 -->
<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="mb-2 font-bold leading-relaxed">违约</div>
<div class="leading-relaxed"> <div class="leading-relaxed">
用户不得利用本服务进行任何损害海南省学宇思网络科技有限公司及其他第三方权益的行为否则海南省学宇思网络科技有限公司有权立即终止为该用户提供本服务并要求用户赔偿损失由此产生的任何后果由用户自行承担与海南省学宇思网络科技有限公司无关 用户不得利用本服务进行任何损害海南海宇大数据有限公司及其他第三方权益的行为否则海南海宇大数据有限公司有权立即终止为该用户提供本服务并要求用户赔偿损失由此产生的任何后果由用户自行承担与海南海宇大数据有限公司无关
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
const router = useRouter(); const router = useRouter();
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import SectionTitle from "@/components/SectionTitle.vue"; import SectionTitle from "@/components/SectionTitle.vue";
import { showToast } from "vant";
const agentStore = useAgentStore(); const agentStore = useAgentStore();
const { isAgent } = storeToRefs(agentStore); 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 preLoanRiskIcon from "@/assets/images/index/preloan_risk_bg.png";
import { toArray } from "lodash"; import { toArray } from "lodash";
function toInquire(name) { 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() { function toInvitation() {
router.push({ name: "invitation" }); router.push({ name: "invitation" });
@@ -36,11 +45,14 @@ function toHistory() {
const toAgent = () => { const toAgent = () => {
router.push({ path: "/promote" }); router.push({ path: "/promote" });
}; };
const toAgentSystemGuide = () => {
router.push({ name: "agentSystemGuide" });
};
const services = ref([ const services = ref([
{ {
title: "个人大数据", title: "个人风险",
name: "personalData", name: "riskassessment",
subtitle: "数据洞察,规避风险", subtitle: "数据洞察,规避风险",
bg: personalDataIcon, bg: personalDataIcon,
goColor: "#6699ff", goColor: "#6699ff",
@@ -90,7 +102,7 @@ const noticeText = ref([]);
// 报告类型列表 // 报告类型列表
const reportTypes = [ const reportTypes = [
"个人大数据", "个人风险",
"婚恋风险", "婚恋风险",
"入职背调", "入职背调",
"家政风险", "家政风险",
@@ -199,6 +211,18 @@ const toBigData = () => {
<div class="mt-8 rounded-2xl overflow-hidden shadow-xl" @click="toAgent"> <div class="mt-8 rounded-2xl overflow-hidden shadow-xl" @click="toAgent">
<img src="@/assets/images/index/index_b_b.png" class="w-full h-full" mode="widthFix" /> <img src="@/assets/images/index/index_b_b.png" class="w-full h-full" mode="widthFix" />
</div> </div>
<!-- 代理系统指南入口 -->
<div class="mt-4 box-border h-14 w-full flex items-center rounded-lg shadow-lg text-gray-700 px-4 "
@click="toAgentSystemGuide">
<div class="mr-4 h-full flex items-center justify-center">
<van-icon name="description" size="24" style="color: #8b5cf6;" />
</div>
<div class="flex-1">
<div class="text-gray-800 font-semibold">代理系统全面指南</div>
<div class="text-xs text-gray-500">了解代理政策等级特权与收益计算</div>
</div>
<img src="@/assets/images/index/right.png" alt="右箭头" class="h-6 w-6" />
</div>
<SectionTitle title="商务合作" class="mt-4" /> <SectionTitle title="商务合作" class="mt-4" />
<div class="mt-2 rounded-xl overflow-hidden bg-white shadow-xl" @click="toCooperation"> <div class="mt-2 rounded-xl overflow-hidden bg-white shadow-xl" @click="toCooperation">
<img src="@/assets/images/index_a_banner.png" class="w-full h-full" alt="大数据服务横幅" mode="widthFix" /> <img src="@/assets/images/index_a_banner.png" class="w-full h-full" alt="大数据服务横幅" mode="widthFix" />
@@ -211,7 +235,7 @@ const toBigData = () => {
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class="text-gray-800">我的历史查询记录</div> <div class="text-gray-800">我的历史查询记录</div>
<div class="text-xs text-gray-500">查询记录有效期为30</div> <div class="text-xs text-gray-500">查询记录有效期为{{ useAppStore().queryRetentionDays || 30 }}</div>
</div> </div>
<img src="@/assets/images/index/right.png" alt="右箭头" class="h-6 w-6" /> <img src="@/assets/images/index/right.png" alt="右箭头" class="h-6 w-6" />
</div> </div>
@@ -227,6 +251,7 @@ const toBigData = () => {
.clip-right { .clip-right {
clip-path: polygon(0 0, 0 0, 90% 100%, 0 0); clip-path: polygon(0 0, 0 0, 90% 100%, 0 0);
} }
.notice-swipe { .notice-swipe {
height: 28px; height: 28px;
line-height: 28px; line-height: 28px;

View File

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

Some files were not shown because too many files have changed in this diff Show More