This commit is contained in:
2026-02-28 17:57:40 +08:00
parent 3bf74c11e0
commit 3bc96be89b
10 changed files with 734 additions and 118 deletions

View File

@@ -41,17 +41,17 @@ onMounted(async () => {
} }
getWeixinAuthUrl(); getWeixinAuthUrl();
// 延迟配置微信分享 // 延迟配置微信分享(带上当前路由信息)
setTimeout(async () => { setTimeout(async () => {
if (isWeChat.value && window.jWeixin) { if (isWeChat.value && window.jWeixin) {
await setDynamicShare(); await setDynamicShare(route);
} }
}, 1000); }, 1000);
// 监听路由变化更新分享配置 // 监听路由变化更新分享配置
router.afterEach(async () => { router.afterEach(async (to) => {
if (isWeChat.value && window.jWeixin && !authStore.isWeixinAuthing) { if (isWeChat.value && window.jWeixin && !authStore.isWeixinAuthing) {
await setDynamicShare(); await setDynamicShare(to);
} }
}); });
}); });

View File

@@ -278,3 +278,68 @@ export function getInviteLink(params) {
const queryString = buildQueryString(params || {}); const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/invite_link${queryString}`).get().json(); return useApiFetch(`/agent/invite_link${queryString}`).get().json();
} }
// ==================== 白名单相关接口 ====================
/**
* 获取可屏蔽的feature列表带价格
*/
export function getWhitelistFeatures() {
return useApiFetch("/agent/whitelist/features").get().json();
}
/**
* 创建白名单订单
* @param {object} params - 创建参数
* @param {string} params.id_card - 身份证号
* @param {string[]} params.feature_ids - 要屏蔽的feature ID列表
* @param {string} params.order_id - 关联的查询订单ID可选
*/
export function createWhitelistOrder(params) {
return useApiFetch("/agent/whitelist/order/create").post(params).json();
}
/**
* 查询白名单列表
* @param {object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
* @param {string} params.id_card - 身份证号(可选,用于筛选)
*/
export function getWhitelistList(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/whitelist/list${queryString}`).get().json();
}
/**
* 检查模块是否已下架
* @param {object} params - 查询参数
* @param {string} params.id_card - 身份证号
* @param {string} params.feature_api_id - Feature的API标识
*/
export function checkFeatureWhitelistStatus(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/whitelist/check${queryString}`).get().json();
}
/**
* 下架单个模块(创建订单并支付/或免费下架)
* @param {object} params - 下架参数
* @param {string} params.id_card - 身份证号
* @param {string} params.feature_api_id - Feature的API标识
* @param {string} params.order_id - 关联的查询订单ID可选
* @param {string} params.query_id - 查询记录ID用于后端删除报告数据
*/
export function offlineFeature(params) {
return useApiFetch("/agent/whitelist/offline").post(params).json();
}
/**
* 检查订单是否属于当前代理推广
* @param {object} params - 查询参数
* @param {string} params.order_id - 订单ID
*/
export function checkOrderAgent(params) {
const queryString = buildQueryString(params || {});
return useApiFetch(`/agent/order/agent${queryString}`).get().json();
}

View File

@@ -3,12 +3,18 @@ import ShareReportButton from "./ShareReportButton.vue";
import TitleBanner from "./TitleBanner.vue"; import TitleBanner from "./TitleBanner.vue";
import VerificationCard from "./VerificationCard.vue"; import VerificationCard from "./VerificationCard.vue";
import StyledTabs from "./StyledTabs.vue"; import StyledTabs from "./StyledTabs.vue";
import WhitelistModuleDialog from "./WhitelistModuleDialog.vue";
import Payment from "./Payment.vue";
import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js'; import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js';
import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js'; 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"; import { useAppStore } from "@/stores/appStore";
import { useAgentStore } from "@/stores/agentStore";
import { storeToRefs } from 'pinia';
import { showFailToast } from 'vant';
import { checkFeatureWhitelistStatus, offlineFeature, checkOrderAgent } from '@/api/agent';
// 动态导入产品背景图片的函数 // 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => { const loadProductBackground = async (productType) => {
@@ -51,6 +57,11 @@ const props = defineProps({
type: String, type: String,
default: "", default: "",
}, },
queryId: {
type: String,
required: false,
default: "",
},
feature: { feature: {
type: String, type: String,
required: true, required: true,
@@ -96,8 +107,26 @@ const {
isEmpty, isEmpty,
isDone, isDone,
isExample, isExample,
orderId,
orderNo,
queryId,
} = toRefs(props); } = toRefs(props);
// 代理信息
const agentStore = useAgentStore()
const { isDiamond } = storeToRefs(agentStore)
// 屏蔽模块弹窗(已废弃,保留用于兼容)
const showWhitelistDialog = ref(false)
// 订单是否属于当前代理推广
const isAgentOrder = ref(false)
// 获取身份证号(从 reportParams 中,用于展示与接口)
const idCard = computed(() => {
return reportParams.value?.id_card || ''
})
const active = ref(null); const active = ref(null);
const backgroundContainerRef = ref(null); // 背景容器的引用 const backgroundContainerRef = ref(null); // 背景容器的引用
@@ -108,15 +137,272 @@ const imageAspectRatio = ref(0); // 缓存图片宽高比
const MAX_BACKGROUND_HEIGHT = 211; // 最大背景高度,防止图片过高变形 const MAX_BACKGROUND_HEIGHT = 211; // 最大背景高度,防止图片过高变形
const trapezoidBgImage = ref(''); // 牌匾背景图片 const trapezoidBgImage = ref(''); // 牌匾背景图片
// 模块下架状态映射主模块ID -> { isOfflined, whitelistPrice, isSubmitting }
const featureOfflineStatus = ref(new Map())
// 提取主模块ID去掉下划线后的部分
const getMainApiId = (apiId) => {
if (!apiId) return ''
const index = apiId.indexOf('_')
return index > 0 ? apiId.substring(0, index) : apiId
}
// 检查模块下架状态
const checkFeatureStatus = async (featureApiId, forceRefresh = false) => {
if (!idCard.value || !featureApiId) return
const mainApiId = getMainApiId(featureApiId)
if (!mainApiId) return
if (!forceRefresh && featureOfflineStatus.value.has(mainApiId)) return
try {
const { data, error } = await checkFeatureWhitelistStatus({
id_card: idCard.value,
feature_api_id: mainApiId,
query_id: queryId.value || '',
})
if (data.value && !error.value && data.value.code === 200) {
const isWhitelisted = data.value.data.is_whitelisted || false
const dataDeleted = data.value.data.data_deleted !== undefined ? data.value.data.data_deleted : true
const status = {
isOfflined: isWhitelisted && dataDeleted,
whitelistPrice: data.value.data.whitelist_price || 0,
isSubmitting: false,
}
featureOfflineStatus.value.set(mainApiId, status)
}
} catch (err) {
console.error('检查模块状态失败:', err)
}
}
// 批量检查所有模块的下架状态
const checkAllFeaturesStatus = async () => {
if (!idCard.value || !isAgentOrder.value || isExample.value) return
const featureApiIds = processedReportData.value.map(item => item.data.apiID)
const mainApiIds = [...new Set(featureApiIds.map(id => getMainApiId(id)))]
for (const mainApiId of mainApiIds) {
if (mainApiId) {
await checkFeatureStatus(mainApiId)
}
}
}
// 获取模块下架状态
const getFeatureStatus = (featureApiId) => {
const mainApiId = getMainApiId(featureApiId)
const status = featureOfflineStatus.value.get(mainApiId) || {
isOfflined: false,
whitelistPrice: 0,
isSubmitting: false,
}
return status
}
// 当前正在下架的模块信息(用于支付确认弹窗)
const currentOfflineFeature = ref(null)
const showOfflineConfirmDialog = ref(false)
// 处理下架按钮点击
const handleOfflineClick = async (featureApiId, featureName) => {
const mainApiId = getMainApiId(featureApiId)
const status = getFeatureStatus(mainApiId)
// 如果已下架,不允许再次点击
if (status.isOfflined) {
showFailToast('该模块已下架')
return
}
// 如果 whitelistPrice = 0直接下架免费不需要支付确认
if (status.whitelistPrice <= 0) {
await confirmOfflineDirectly(mainApiId, featureName)
return
}
// 如果 whitelistPrice > 0需要支付确认
currentOfflineFeature.value = {
featureApiId: mainApiId,
featureName,
whitelistPrice: status.whitelistPrice,
}
showOfflineConfirmDialog.value = true
}
// 白名单下架支付弹窗相关状态
const showWhitelistPayment = ref(false)
const whitelistPaymentData = ref({ product_name: '', sell_price: 0 })
const whitelistPaymentId = ref('')
const whitelistPaymentType = ref('whitelist')
// 获取当前报告页面的 URL用于支付成功后返回
const getCurrentReportUrl = () => {
if (orderNo.value) {
return `/report?orderNo=${orderNo.value}`
} else if (orderId.value) {
return `/report?orderId=${orderId.value}`
}
return ''
}
// 直接下架(免费)
const confirmOfflineDirectly = async (mainApiId, featureName) => {
if (!idCard.value || !mainApiId) return
const status = getFeatureStatus(mainApiId)
status.isSubmitting = true
featureOfflineStatus.value.set(mainApiId, { ...status })
try {
if (!queryId.value) {
showFailToast('缺少查询记录ID无法下架')
const currentStatus = getFeatureStatus(mainApiId)
currentStatus.isSubmitting = false
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
return
}
const { data, error } = await offlineFeature({
query_id: queryId.value,
feature_api_id: mainApiId,
})
if (!data.value || error.value || data.value.code !== 200) {
showFailToast(data.value?.msg || '下架失败')
const currentStatus = getFeatureStatus(mainApiId)
currentStatus.isSubmitting = false
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
return
}
const resp = data.value.data || {}
if (resp.need_pay) {
const currentStatus = getFeatureStatus(mainApiId)
currentStatus.isSubmitting = false
currentStatus.whitelistPrice = resp.amount || 0
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
whitelistPaymentData.value = {
product_name: `${featureName || '模块'} 下架`,
sell_price: resp.amount || 0,
}
whitelistPaymentId.value = `${idCard.value}|${mainApiId}`
whitelistPaymentType.value = 'whitelist'
showWhitelistPayment.value = true
return
}
showFailToast('下架成功')
const updatedStatus = getFeatureStatus(mainApiId)
updatedStatus.isSubmitting = false
updatedStatus.isOfflined = true
featureOfflineStatus.value.set(mainApiId, { ...updatedStatus })
if (queryId.value || orderId.value) {
window.location.reload()
}
} catch (err) {
console.error('下架模块失败:', err)
showFailToast('下架模块失败')
const currentStatus = getFeatureStatus(mainApiId)
currentStatus.isSubmitting = false
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
}
}
// 确认下架(付费场景)
const confirmOffline = async () => {
if (!currentOfflineFeature.value) return
const { featureApiId } = currentOfflineFeature.value
const mainApiId = featureApiId
if (!queryId.value) {
showFailToast('缺少查询记录ID无法下架')
return
}
const status = getFeatureStatus(mainApiId)
status.isSubmitting = true
featureOfflineStatus.value.set(mainApiId, { ...status })
try {
const { data, error } = await offlineFeature({
query_id: queryId.value,
feature_api_id: mainApiId,
})
if (!data.value || error.value || data.value.code !== 200) {
showFailToast(data.value?.msg || '下架失败')
const currentStatus = getFeatureStatus(mainApiId)
currentStatus.isSubmitting = false
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
return
}
const resp = data.value.data || {}
if (resp.need_pay) {
showOfflineConfirmDialog.value = false
whitelistPaymentData.value = {
product_name: `${currentOfflineFeature.value?.featureName || '模块'} 下架`,
sell_price: resp.amount || 0,
}
whitelistPaymentId.value = `${idCard.value}|${mainApiId}`
whitelistPaymentType.value = 'whitelist'
showWhitelistPayment.value = true
const currentStatus = getFeatureStatus(mainApiId)
currentStatus.isSubmitting = false
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
return
}
showFailToast('下架成功')
showOfflineConfirmDialog.value = false
currentOfflineFeature.value = null
const updatedStatus = getFeatureStatus(mainApiId)
updatedStatus.isSubmitting = false
featureOfflineStatus.value.set(mainApiId, { ...updatedStatus })
if (queryId.value || orderId.value) {
window.location.reload()
}
} catch (err) {
console.error('下架模块失败:', err)
showFailToast('下架模块失败')
const currentStatus = getFeatureStatus(mainApiId)
currentStatus.isSubmitting = false
featureOfflineStatus.value.set(mainApiId, { ...currentStatus })
}
}
// 打开屏蔽模块弹窗(已废弃,保留用于兼容)
const openWhitelistDialog = () => {
if (!idCard.value) {
console.error('无法获取身份证号')
return
}
showWhitelistDialog.value = true
}
// 屏蔽成功后的回调(已废弃,保留用于兼容)
const onWhitelistSuccess = () => {
console.log('模块已屏蔽')
}
// 计算背景高度 // 计算背景高度
const calculateBackgroundHeight = () => { const calculateBackgroundHeight = () => {
if (imageAspectRatio.value > 0) { if (imageAspectRatio.value > 0) {
// 获取容器的实际宽度,而不是整个窗口宽度
const containerWidth = backgroundContainerRef.value const containerWidth = backgroundContainerRef.value
? backgroundContainerRef.value.offsetWidth ? backgroundContainerRef.value.offsetWidth
: window.innerWidth; : window.innerWidth;
const calculatedHeight = containerWidth * imageAspectRatio.value; const calculatedHeight = containerWidth * imageAspectRatio.value;
// 限制最大高度,防止图片过高
backgroundHeight.value = Math.min(calculatedHeight, MAX_BACKGROUND_HEIGHT); backgroundHeight.value = Math.min(calculatedHeight, MAX_BACKGROUND_HEIGHT);
} }
}; };
@@ -126,13 +412,10 @@ const loadBackgroundImage = async () => {
const background = await loadProductBackground(feature.value); const background = await loadProductBackground(feature.value);
productBackground.value = background || ''; productBackground.value = background || '';
// 加载图片后计算高度
if (background) { if (background) {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
// 缓存图片宽高比
imageAspectRatio.value = img.height / img.width; imageAspectRatio.value = img.height / img.width;
// 图片加载完成后,等待下一帧再计算高度,确保容器已渲染
nextTick(() => { nextTick(() => {
calculateBackgroundHeight(); calculateBackgroundHeight();
}); });
@@ -149,8 +432,23 @@ onMounted(async () => {
await loadBackgroundImage(); await loadBackgroundImage();
await loadTrapezoidBackground(); await loadTrapezoidBackground();
// 监听窗口大小变化,重新计算高度
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
// 检查订单是否属于当前代理推广
if (!isExample.value && orderId.value) {
try {
const { data, error } = await checkOrderAgent({ order_id: orderId.value })
if (data.value && !error.value && data.value.code === 200) {
isAgentOrder.value = data.value.data.is_agent_order
}
} catch (err) {
console.error('检查订单代理状态失败:', err)
}
}
if (isAgentOrder.value && idCard.value && !isExample.value) {
checkAllFeaturesStatus()
}
}); });
// 处理窗口大小变化(带防抖) // 处理窗口大小变化(带防抖)
@@ -160,7 +458,7 @@ const handleResize = () => {
} }
resizeTimer = setTimeout(() => { resizeTimer = setTimeout(() => {
calculateBackgroundHeight(); calculateBackgroundHeight();
}, 100); // 100ms 防抖延迟 }, 100);
}; };
// 组件卸载时移除监听器 // 组件卸载时移除监听器
@@ -171,23 +469,13 @@ onUnmounted(() => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
}); });
// 处理数据拆分支持DWBG8B4D、DWBG6A2C、CJRZQ5E9F和CQYGL3F8E // 处理数据拆分
const processedReportData = computed(() => { const processedReportData = computed(() => {
let data = reportData.value; let data = reportData.value;
// 拆分DWBG8B4D数据
data = splitDWBG8B4DForTabs(data); data = splitDWBG8B4DForTabs(data);
// 拆分DWBG6A2C数据
data = splitDWBG6A2CForTabs(data); data = splitDWBG6A2CForTabs(data);
// 拆分JRZQ7F1A数据
data = splitJRZQ7F1AForTabs(data); data = splitJRZQ7F1AForTabs(data);
// // 拆分CJRZQ5E9F数据
// data = splitCJRZQ5E9FForTabs(data);
// 拆分CQYGL3F8E数据
data = splitCQYGL3F8EForTabs(data); data = splitCQYGL3F8EForTabs(data);
// 过滤掉在featureMap中没有对应的项
return data.filter(item => featureMap[item.data.apiID]); return data.filter(item => featureMap[item.data.apiID]);
}); });
@@ -202,7 +490,7 @@ const backgroundContainerStyle = computed(() => {
}; };
} }
return { return {
height: '180px', // 默认高度 height: '180px',
}; };
}); });
@@ -211,10 +499,10 @@ const backgroundImageStyle = computed(() => {
if (getProductBackground.value) { if (getProductBackground.value) {
return { return {
backgroundImage: `url(${getProductBackground.value})`, backgroundImage: `url(${getProductBackground.value})`,
backgroundSize: '100% auto', // 宽度100%,高度自动保持比例 backgroundSize: '100% auto',
backgroundPosition: 'center', // 向上偏移20px backgroundPosition: 'center',
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
overflow: 'hidden', // 超出部分裁剪 overflow: 'hidden',
}; };
} }
return {}; return {};
@@ -752,10 +1040,8 @@ const calculateScore = () => {
const apiID = item.data?.apiID; const apiID = item.data?.apiID;
if (!apiID) return; if (!apiID) return;
// 获取风险权重(如果不在配置中,默认为 3
const weight = featureRiskLevels[apiID] ?? 3; const weight = featureRiskLevels[apiID] ?? 3;
// 跳过权重为 0 的复合报告主模块(它们由子模块计算)
if (weight === 0) return; if (weight === 0) return;
presentFeatures.push({ presentFeatures.push({
@@ -765,49 +1051,21 @@ const calculateScore = () => {
}); });
}); });
if (presentFeatures.length === 0) return 100; // 无有效特征时返回满分(最安全) if (presentFeatures.length === 0) return 100;
// 累计总风险分数
let totalRiskScore = 0; let totalRiskScore = 0;
const riskDetails = []; // 用于调试
presentFeatures.forEach(({ apiID, index, weight }) => { presentFeatures.forEach(({ apiID, index, weight }) => {
// 从组件风险评分中获取评分0-100分分数越高越安全
const key = `${apiID}_${index}`; const key = `${apiID}_${index}`;
const componentScore = componentRiskScores.value[key] ?? 100; // 默认100分最安全 const componentScore = componentRiskScores.value[key] ?? 100;
// 将组件评分转换为风险分数0-100 -> 100-0
const componentRisk = 100 - componentScore; const componentRisk = 100 - componentScore;
const weightMultiplier = 1.5;
// 计算该模块的风险贡献(固定分值,不按占比)
// 使用权重系数放大高风险模块的影响
// 高风险模块权重10如果风险分数是0扣20分权重10 × 系数2
// 中风险模块权重7如果风险分数是0扣14分权重7 × 系数2
// 低风险模块权重3如果风险分数是0扣6分权重3 × 系数2
const weightMultiplier = 1.5; // 权重系数,可以调整这个值来控制影响程度
const riskContribution = (componentRisk / 100) * weight * weightMultiplier; const riskContribution = (componentRisk / 100) * weight * weightMultiplier;
riskDetails.push({
apiID,
index,
weight,
componentScore,
componentRisk,
riskContribution,
hasStatus: key in componentRiskScores.value
});
// 累加风险分数
totalRiskScore += riskContribution; totalRiskScore += riskContribution;
}); });
// 将总风险分数限制在 0-90 范围内确保最低分为10分
const finalRiskScore = Math.max(0, Math.min(90, Math.round(totalRiskScore))); const finalRiskScore = Math.max(0, Math.min(90, Math.round(totalRiskScore)));
// 转换为安全分数分数越高越安全100 - 风险分数)
// 最终分数范围10-100分
const safetyScore = 100 - finalRiskScore; const safetyScore = 100 - finalRiskScore;
return safetyScore; return safetyScore;
}; };
@@ -815,21 +1073,6 @@ const calculateScore = () => {
watch([reportData, componentRiskScores], () => { watch([reportData, componentRiskScores], () => {
reportScore.value = calculateScore(); reportScore.value = calculateScore();
// 将评分系统数据整理到一个对象中
const scoreData = {
timestamp: new Date().toISOString(),
finalScore: reportScore.value,
reportModules: processedReportData.value.map((item, index) => ({
apiID: item.data.apiID,
name: featureMap[item.data.apiID]?.name || '未知',
index: index,
riskScore: componentRiskScores.value[`${item.data.apiID}_${index}`] ?? '未上报',
weight: featureRiskLevels[item.data.apiID] ?? 0
})),
componentScores: componentRiskScores.value,
riskLevels: featureRiskLevels
};
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
// 从环境变量获取配置 // 从环境变量获取配置

View File

@@ -0,0 +1,227 @@
<template>
<van-popup v-model:show="show" position="bottom" round :style="{ height: '70%' }" class="whitelist-module-dialog">
<div class="flex flex-col h-full">
<!-- 标题栏 -->
<div class="flex items-center justify-between p-4 border-b">
<h3 class="text-lg font-bold">屏蔽模块</h3>
<van-icon name="cross" size="20" @click="close" />
</div>
<!-- 内容区域 -->
<div class="flex-1 overflow-y-auto p-4">
<div v-if="loading" class="flex items-center justify-center h-40">
<van-loading type="spinner" />
</div>
<div v-else-if="featureList.length === 0" class="flex items-center justify-center h-40 text-gray-500">
暂无可屏蔽的模块
</div>
<div v-else>
<div class="mb-4 text-sm text-gray-600">
选择要屏蔽的模块屏蔽后该身份证号查询时将不显示这些模块的数据
</div>
<!-- 模块列表 -->
<van-checkbox-group v-model="selectedFeatureIds">
<div v-for="feature in featureList" :key="feature.feature_id" class="mb-3">
<van-cell :title="feature.feature_name" :label="`价格:¥${feature.whitelist_price.toFixed(2)}`"
clickable @click="toggleFeature(feature.feature_id)">
<template #right-icon>
<van-checkbox :name="feature.feature_id" />
</template>
</van-cell>
</div>
</van-checkbox-group>
</div>
</div>
<!-- 底部操作栏 -->
<div class="p-4 border-t bg-gray-50">
<div class="mb-3 flex items-center justify-between">
<span class="text-sm text-gray-600">已选择{{ selectedFeatureIds.length }} 个模块</span>
<span class="text-lg font-bold text-red-500">
总计¥{{ totalAmount.toFixed(2) }}
</span>
</div>
<van-button type="primary" block round :loading="isSubmitting"
:disabled="selectedFeatureIds.length === 0" @click="handleConfirm">
确认屏蔽
</van-button>
</div>
</div>
<!-- 支付弹窗 -->
<Payment v-model="showPayment" :data="paymentData"
:id="paymentId" :type="paymentType" />
</van-popup>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { getWhitelistFeatures, createWhitelistOrder } from '@/api/agent'
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant'
import { useAgentStore } from '@/stores/agentStore'
import { storeToRefs } from 'pinia'
import Payment from './Payment.vue'
const props = defineProps({
idCard: {
type: String,
required: true,
},
orderId: {
type: String,
default: '',
},
})
const show = defineModel('show', { type: Boolean, default: false })
const agentStore = useAgentStore()
const { isDiamond } = storeToRefs(agentStore)
const loading = ref(false)
const featureList = ref([])
const selectedFeatureIds = ref([])
const isSubmitting = ref(false)
// 支付相关状态
const showPayment = ref(false)
const paymentData = ref({
product_name: '',
sell_price: 0,
})
const paymentId = ref('')
const paymentType = ref('whitelist')
const currentOrderId = ref('')
// 计算总金额
const totalAmount = computed(() => {
return featureList.value
.filter(f => selectedFeatureIds.value.includes(f.feature_id))
.reduce((sum, f) => sum + f.whitelist_price, 0)
})
// 切换选择
const toggleFeature = (featureId) => {
const index = selectedFeatureIds.value.indexOf(featureId)
if (index > -1) {
selectedFeatureIds.value.splice(index, 1)
} else {
selectedFeatureIds.value.push(featureId)
}
}
// 关闭弹窗
const close = () => {
show.value = false
selectedFeatureIds.value = []
}
// 加载可屏蔽的模块列表
const loadFeatures = async () => {
loading.value = true
try {
const { data, error } = await getWhitelistFeatures()
if (data.value && !error.value && data.value.code === 200) {
featureList.value = data.value.data.list || []
} else {
showFailToast(data.value?.msg || '获取模块列表失败')
}
} catch (err) {
console.error('获取模块列表失败:', err)
showFailToast('获取模块列表失败')
} finally {
loading.value = false
}
}
// 确认屏蔽
const handleConfirm = async () => {
if (selectedFeatureIds.value.length === 0) {
showFailToast('请至少选择一个模块')
return
}
// 确认对话框
try {
await showConfirmDialog({
title: '确认屏蔽',
message: `确定要屏蔽 ${selectedFeatureIds.value.length} 个模块吗?总费用:¥${totalAmount.value.toFixed(2)}`,
})
} catch {
return // 用户取消
}
isSubmitting.value = true
try {
// 1. 创建订单
const { data: orderData, error: orderError } = await createWhitelistOrder({
id_card: props.idCard,
feature_ids: selectedFeatureIds.value,
order_id: props.orderId || undefined,
})
if (!orderData.value || orderError.value || orderData.value.code !== 200) {
showFailToast(orderData.value?.msg || '创建订单失败')
return
}
const orderId = orderData.value.data.order_id
const orderNo = orderData.value.data.order_no
const totalAmount = orderData.value.data.total_amount
// 2. 使用统一支付组件进行支付
// PaymentReq.Id 约定格式:白名单订单使用 "{idCard}|{featureApiId}" 格式
// 但批量订单使用订单号,需要根据后端接口调整
// 这里先使用订单号,如果后端需要特定格式,需要调整
paymentData.value = {
product_name: `模块屏蔽(${selectedFeatureIds.value.length}个模块)`,
sell_price: totalAmount,
}
paymentId.value = orderNo // 使用订单号作为支付ID
paymentType.value = 'whitelist'
currentOrderId.value = orderId
showPayment.value = true
} catch (err) {
console.error('屏蔽模块失败:', err)
showFailToast('屏蔽模块失败')
} finally {
isSubmitting.value = false
}
}
// 监听支付弹窗关闭,如果支付成功会跳转到支付结果页面
watch(showPayment, (newVal) => {
if (!newVal && currentOrderId.value) {
// 支付弹窗关闭,可能是支付完成(会跳转到结果页面)或用户取消
// 这里不做特殊处理,因为支付成功会跳转到结果页面
currentOrderId.value = ''
}
})
const emit = defineEmits(['success'])
// 监听弹窗显示,加载数据
watch(show, (newVal) => {
if (newVal) {
// 验证是否为钻石代理
if (!isDiamond.value) {
showFailToast('只有钻石代理可以操作白名单')
close()
return
}
loadFeatures()
selectedFeatureIds.value = []
}
})
</script>
<style scoped>
.whitelist-module-dialog {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -185,43 +185,38 @@ export function useWeixinShare() {
}; };
/** /**
* 根据当前页面动态设置分享内容 * 根据当前路由动态设置分享内容
* @param {object} routeInfo - vue-router 的 route 对象,可选
*/ */
const setDynamicShare = async () => { const setDynamicShare = async (routeInfo) => {
const route = window.location.pathname; // 某些页面(如 inquire / promotionInquire / example有自定义分享逻辑
let shareConfig = {}; if (routeInfo && routeInfo.meta && routeInfo.meta.shareCustom) {
return;
// 根据不同的路由设置不同的分享内容
if (route.includes("/example")) {
shareConfig = {
title: import.meta.env.VITE_SHARE_TITLE,
desc: import.meta.env.VITE_SHARE_DESC,
link: window.location.href.split("#")[0],
imgUrl: import.meta.env.VITE_SHARE_IMG,
};
} else if (route.includes("/agent")) {
shareConfig = {
title: import.meta.env.VITE_SHARE_TITLE,
desc: import.meta.env.VITE_SHARE_DESC,
link: window.location.href.split("#")[0],
imgUrl: import.meta.env.VITE_SHARE_IMG,
};
} else if (route.includes("/help")) {
shareConfig = {
title: import.meta.env.VITE_SHARE_TITLE,
desc: import.meta.env.VITE_SHARE_DESC,
link: window.location.href.split("#")[0],
imgUrl: import.meta.env.VITE_SHARE_IMG,
};
} else {
shareConfig = {
title: import.meta.env.VITE_SHARE_TITLE,
desc: import.meta.env.VITE_SHARE_DESC,
link: window.location.href.split("#")[0],
imgUrl: import.meta.env.VITE_SHARE_IMG,
};
} }
const fullPath = window.location.href.split("#")[0];
const baseShareTitle = import.meta.env.VITE_SHARE_TITLE || "";
const appName = import.meta.env.VITE_APP_NAME || "";
const routeMetaTitle = routeInfo && routeInfo.meta && routeInfo.meta.title;
const routeName = routeInfo && routeInfo.name;
let title = baseShareTitle;
// 除首页之外,如果有路由标题,则加到分享标题中
if (routeMetaTitle && routeName !== "index") {
title = appName
? `${routeMetaTitle}${appName}`
: `${routeMetaTitle}${baseShareTitle}`;
}
const shareConfig = {
title,
desc: import.meta.env.VITE_SHARE_DESC,
link: fullPath,
imgUrl: import.meta.env.VITE_SHARE_IMG,
};
await configWeixinShare(shareConfig); await configWeixinShare(shareConfig);
}; };

View File

@@ -132,7 +132,7 @@ const router = createRouter({
path: "/example", path: "/example",
name: "example", name: "example",
component: () => import("@/views/Example.vue"), component: () => import("@/views/Example.vue"),
meta: { title: "示例报告", notNeedBindPhone: true }, meta: { title: "示例报告", notNeedBindPhone: true, shareCustom: true },
}, },
{ {
path: "/vant-theme-test", path: "/vant-theme-test",
@@ -173,7 +173,7 @@ const router = createRouter({
path: "/inquire/:feature", path: "/inquire/:feature",
name: "inquire", name: "inquire",
component: () => import("@/views/Inquire.vue"), component: () => import("@/views/Inquire.vue"),
meta: { title: "查询报告" }, meta: { title: "查询报告", shareCustom: true },
}, },
{ {
path: "/authorization", path: "/authorization",
@@ -398,7 +398,7 @@ const router = createRouter({
path: "/agent/promotionInquire/:linkIdentifier", path: "/agent/promotionInquire/:linkIdentifier",
name: "promotionInquire", name: "promotionInquire",
component: () => import("@/views/PromotionInquire.vue"), component: () => import("@/views/PromotionInquire.vue"),
meta: { notNeedBindPhone: true }, meta: { notNeedBindPhone: true, shareCustom: true },
}, },
{ {
path: "/agent/invitationAgentApply/:linkIdentifier", path: "/agent/invitationAgentApply/:linkIdentifier",

View File

@@ -8,6 +8,7 @@
<script setup> <script setup>
import { aesDecrypt } from "@/utils/crypto"; import { aesDecrypt } from "@/utils/crypto";
import { useWeixinShare } from "@/composables/useWeixinShare";
const AES_KEY = import.meta.env.VITE_INQUIRE_AES_KEY; const AES_KEY = import.meta.env.VITE_INQUIRE_AES_KEY;
@@ -20,6 +21,18 @@ const isEmpty = ref(false);
const isDone = ref(false); const isDone = ref(false);
const active = ref(0); const active = ref(0);
const { configWeixinShare } = useWeixinShare();
const updateExampleWeixinShare = async () => {
if (!reportName.value) return;
const baseTitle = reportName.value;
const title = `${baseTitle}(示例报告)`;
await configWeixinShare({
title,
desc: import.meta.env.VITE_SHARE_DESC,
});
};
onBeforeMount(() => { onBeforeMount(() => {
const query = new URLSearchParams(window.location.search); const query = new URLSearchParams(window.location.search);
@@ -72,6 +85,9 @@ const getReport = async () => {
reportParams.value = decryptedData.query_params || {}; reportParams.value = decryptedData.query_params || {};
reportName.value = decryptedData.product_name || ""; reportName.value = decryptedData.product_name || "";
reportDateTime.value = decryptedData.create_time || null; reportDateTime.value = decryptedData.create_time || null;
// 更新示例报告的微信分享文案
await updateExampleWeixinShare();
} else if (data.value.code === 200003) { } else if (data.value.code === 200003) {
isEmpty.value = true; isEmpty.value = true;
} }

View File

@@ -1,8 +1,9 @@
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useAgentStore } from "@/stores/agentStore"; import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore"; import { useUserStore } from "@/stores/userStore";
import { useWeixinShare } from "@/composables/useWeixinShare";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { showToast } from "vant"; import { showToast } from "vant";
@@ -18,6 +19,31 @@ const feature = ref(route.params.feature);
// 获取产品信息 // 获取产品信息
const featureData = ref({}); const featureData = ref({});
// 微信分享
const { configWeixinShare } = useWeixinShare();
// 去掉富文本 HTML 标签,保留纯文本
const stripHtml = (html) => {
if (!html) return "";
return html.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
};
const updateWeixinShareForProduct = async () => {
if (!featureData.value || !featureData.value.product_name) return;
const appName = import.meta.env.VITE_APP_NAME || "";
const baseTitle = featureData.value.product_name;
const title = appName ? `${baseTitle}${appName}风险报告` : baseTitle;
const desc =
stripHtml(featureData.value.description) ||
import.meta.env.VITE_SHARE_DESC;
await configWeixinShare({
title,
desc,
});
};
// 检查登录状态 // 检查登录状态
onMounted(async () => { onMounted(async () => {
// 检查支付回调 // 检查支付回调
@@ -64,6 +90,9 @@ async function getProduct() {
return 0; return 0;
}); });
} }
console.log('featureData', featureData.value);
// 更新微信分享文案
await updateWeixinShareForProduct();
} }
} }
</script> </script>

View File

@@ -31,18 +31,25 @@
<span class="text-gray-800">{{ <span class="text-gray-800">{{
paymentType === "agent_upgrade" paymentType === "agent_upgrade"
? "代理升级" ? "代理升级"
: paymentType === "whitelist"
? "模块下架"
: "查询服务" : "查询服务"
}}</span> }}</span>
</div> </div>
</div> </div>
<div v-if="paymentType === 'agent_upgrade'" class="text-center text-gray-600 mb-4">恭喜你升级代理等级成功享受更多权益 <div v-if="paymentType === 'agent_upgrade'" class="text-center text-gray-600 mb-4">恭喜你升级代理等级成功享受更多权益
</div> </div>
<div v-else-if="paymentType === 'whitelist'" 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_upgrade" paymentType === "agent_upgrade"
? "查看代理信息" ? "查看代理信息"
: paymentType === "whitelist"
? "返回报告"
: "查看查询结果" : "查看查询结果"
}} }}
</van-button> </van-button>
@@ -74,6 +81,8 @@
<span class="text-gray-800">{{ <span class="text-gray-800">{{
paymentType === "agent_upgrade" paymentType === "agent_upgrade"
? "代理升级" ? "代理升级"
: paymentType === "whitelist"
? "模块下架"
: "查询服务" : "查询服务"
}}</span> }}</span>
</div> </div>
@@ -137,6 +146,8 @@
<span class="text-gray-800">{{ <span class="text-gray-800">{{
paymentType === "agent_upgrade" paymentType === "agent_upgrade"
? "代理升级" ? "代理升级"
: paymentType === "whitelist"
? "模块下架"
: "查询服务" : "查询服务"
}}</span> }}</span>
</div> </div>
@@ -329,6 +340,25 @@ const checkPaymentStatus = async () => {
return; return;
} }
// 对于白名单类型,如果状态是已支付,跳转回报告页面
if (
paymentType.value === "whitelist" &&
newStatus === "paid"
) {
stopPolling();
// 优先使用 returnUrl否则返回上一页
const returnUrl = route.query.returnUrl;
if (returnUrl) {
router.replace(returnUrl);
} else if (window.history.length > 1) {
router.go(-1);
} else {
// 如果没有历史记录,跳转到首页
router.replace("/");
}
return;
}
// 如果状态不是 pending停止轮询 // 如果状态不是 pending停止轮询
if (newStatus !== "pending") { if (newStatus !== "pending") {
stopPolling(); stopPolling();
@@ -414,6 +444,17 @@ function handleNavigation() {
router.replace("/agent"); router.replace("/agent");
agentStore.fetchAgentStatus(); agentStore.fetchAgentStatus();
userStore.fetchUserInfo(); userStore.fetchUserInfo();
} else if (paymentType.value === "whitelist") {
// 白名单支付:优先使用 returnUrl否则返回上一页
const returnUrl = route.query.returnUrl;
if (returnUrl) {
router.replace(returnUrl);
} else if (window.history.length > 1) {
router.go(-1);
} else {
// 如果没有历史记录,跳转到首页
router.replace("/");
}
} else { } else {
// 跳转到查询结果页面 // 跳转到查询结果页面
router.replace({ router.replace({

View File

@@ -15,8 +15,8 @@ export default defineConfig({
strictPort: true, // 如果端口被占用则抛出错误而不是使用下一个可用端口 strictPort: true, // 如果端口被占用则抛出错误而不是使用下一个可用端口
proxy: { proxy: {
"/api/v1": { "/api/v1": {
target: "http://127.0.0.1:8888", // 本地接口地址 // target: "http://127.0.0.1:8888", // 本地接口地址
// target: "https://yuyuecha.com", // 本地接口地址 target: "https://www.yuyuecha.com", // 本地接口地址
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path, // 可选:确保路径不被修改 rewrite: (path) => path, // 可选:确保路径不被修改
}, },