605 lines
20 KiB
Vue
605 lines
20 KiB
Vue
<template>
|
||
<div class="min-h-screen promote">
|
||
<!-- 顶部背景图 -->
|
||
<div class="promote-header-bg">
|
||
<img src="@/assets/images/promote/promote_bg.jpg" alt="推广背景" class="bg-image" />
|
||
</div>
|
||
|
||
<div class="p-4">
|
||
<!-- 价格设置卡片 -->
|
||
<div class="card mb-4 card-with-header">
|
||
<!-- 优雅的报告头部 -->
|
||
<div class="report-header-elegant">
|
||
<div class="report-header-content">
|
||
<div class="report-logo-container" v-if="currentLogo">
|
||
<div class="report-logo-wrapper">
|
||
<img :src="currentLogo" :alt="pickerFieldText" class="report-logo-elegant" />
|
||
</div>
|
||
</div>
|
||
<div class="report-info-elegant">
|
||
<div class="report-label-elegant">推广报告</div>
|
||
<div class="report-title-elegant">{{ pickerFieldText || '请选择报告类型' }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="report-action-elegant" @click="showTypePicker = true">
|
||
<div class="report-action-icon">
|
||
<van-icon name="exchange" size="16" color="#ffffff" />
|
||
</div>
|
||
<span class="report-action-text">切换</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-4 pt-6">
|
||
<!-- 当前价格显示 -->
|
||
<div class="flex items-center justify-between py-3 border-b border-gray-100">
|
||
<label class="font-medium text-gray-700">客户查询价</label>
|
||
<span class="text-lg font-semibold text-orange-500">¥{{ clientPrice || '0.00' }}</span>
|
||
</div>
|
||
|
||
<!-- 成本和收益信息 -->
|
||
<div class="bg-gray-50 rounded-lg p-3 space-y-2">
|
||
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-sm text-gray-600">底价成本</span>
|
||
<span class="text-sm font-semibold text-orange-500">¥{{ baseCost }}</span>
|
||
</div>
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-sm text-gray-600">提价成本</span>
|
||
<span class="text-sm font-semibold text-orange-500">¥{{ raiseCost }}</span>
|
||
</div>
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-sm text-gray-600">推广收益</span>
|
||
<span class="text-sm font-semibold text-orange-500">¥{{ promotionRevenue }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮组 -->
|
||
<div class="grid grid-cols-3 gap-3 mt-4">
|
||
<van-button type="primary" size="large" class="custom-btn-primary"
|
||
@click="showPricePicker = true">设置价格</van-button>
|
||
<van-button type="primary" size="large" class="custom-btn-primary"
|
||
@click="generatePromotionCode" :disabled="!clientPrice || !currentFeature">推广报告</van-button>
|
||
<van-button type="default" size="large" class="custom-btn-default" @click="toExample"
|
||
:disabled="!currentFeature">示例报告</van-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 报告信息卡片 -->
|
||
<div v-if="featureData.product_name" class="card mb-4">
|
||
<ReportFeatures :features="featureData.features" />
|
||
</div>
|
||
|
||
<!-- 报告类型选择弹窗 -->
|
||
<van-popup v-model:show="showTypePicker" destroy-on-close round position="bottom">
|
||
<van-picker :model-value="selectedReportType" :columns="reportTypes" @cancel="showTypePicker = false"
|
||
@confirm="onConfirmType" />
|
||
</van-popup>
|
||
|
||
<!-- 价格设置弹窗 -->
|
||
<PriceInputPopup v-model:show="showPricePicker" :default-price="clientPrice"
|
||
:product-config="pickerProductConfig" @change="onPriceChange" />
|
||
|
||
<!-- 二维码弹窗 -->
|
||
<QRcode v-model:show="showQRcode" :fullLink="fullLink" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
|
||
<script setup>
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
import PriceInputPopup from '@/components/PriceInputPopup.vue';
|
||
import QRcode from '@/components/QRcode.vue';
|
||
import ReportFeatures from '@/components/ReportFeatures.vue';
|
||
import { getProductConfig, generateLink } from '@/api/agent';
|
||
|
||
// 导入logo图片
|
||
import personalDataLogo from '@/assets/images/promote/personal_data_logo.png';
|
||
import companyLogo from '@/assets/images/promote/company_logo.png';
|
||
import preloanBackgroundCheckLogo from '@/assets/images/promote/preloan_background_check_logo.png';
|
||
import marriageRiskLogo from '@/assets/images/promote/marriage_risk_logo.png';
|
||
import housekeepingRiskLogo from '@/assets/images/promote/housekeeping_risk_logo.png';
|
||
import backgroundcheckLogo from '@/assets/images/promote/backgroundcheck_logo.png';
|
||
import consumerFinanceReportLogo from '@/assets/images/promote/consumer_finance_report_logo.png';
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
// 报告类型配置(从接口获取)
|
||
const reportTypes = ref([]);
|
||
|
||
// 从 query 参数获取报告类型(使用 computed 以响应路由变化)
|
||
const currentFeature = computed(() => route.query.feature || '');
|
||
const showPricePicker = ref(false);
|
||
const showTypePicker = ref(false);
|
||
const pickerFieldText = ref('');
|
||
const selectedReportType = ref([]);
|
||
const pickerProductConfig = ref(null);
|
||
const pickerFieldVal = ref(null); // 保持原来的变量名,用于存储报告类型的 value
|
||
const clientPrice = ref(null);
|
||
const productConfig = ref(null);
|
||
const fullLink = ref(""); // 完整的推广短链
|
||
const featureData = ref({});
|
||
const showQRcode = ref(false);
|
||
|
||
// Logo映射
|
||
const logoMap = {
|
||
'riskassessment': personalDataLogo,
|
||
'companyinfo': companyLogo,
|
||
'preloanbackgroundcheck': preloanBackgroundCheckLogo,
|
||
'marriage': marriageRiskLogo,
|
||
'homeservice': housekeepingRiskLogo,
|
||
'backgroundcheck': backgroundcheckLogo,
|
||
'consumerFinanceReport': consumerFinanceReportLogo,
|
||
};
|
||
|
||
// 当前报告的logo
|
||
const currentLogo = computed(() => {
|
||
if (!currentFeature.value) return null;
|
||
return logoMap[currentFeature.value] || null;
|
||
});
|
||
|
||
const costPrice = computed(() => {
|
||
if (!pickerProductConfig.value) return 0.00
|
||
|
||
// 新系统:成本价 = 实际底价(actual_base_price)
|
||
// actual_base_price = base_price + 等级加成
|
||
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0;
|
||
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;
|
||
}
|
||
|
||
// 总成本 = 实际底价 + 提价成本
|
||
const totalCost = actualBasePrice + priceCost;
|
||
|
||
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 clientPriceNum = Number(clientPrice.value) || 0;
|
||
const costPriceNum = parseFloat(costPrice.value) || 0; // costPrice 返回字符串,需要转换为数字
|
||
const revenue = clientPriceNum - costPriceNum;
|
||
return safeTruncate(revenue >= 0 ? revenue : 0); // 确保收益不为负数
|
||
});
|
||
|
||
function safeTruncate(num, decimals = 2) {
|
||
if (isNaN(num) || !isFinite(num)) return "0.00";
|
||
|
||
const factor = 10 ** decimals;
|
||
const scaled = Math.trunc(num * factor);
|
||
const truncated = scaled / factor;
|
||
|
||
return truncated.toFixed(decimals);
|
||
}
|
||
// 获取产品信息
|
||
const getProductInfo = async () => {
|
||
if (!currentFeature.value) {
|
||
console.warn('No feature parameter found in query');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { data, error } = await useApiFetch(`/product/en/${currentFeature.value}`)
|
||
.get()
|
||
.json();
|
||
|
||
if (data.value && !error.value) {
|
||
featureData.value = data.value.data;
|
||
// 确保 FLXG0V4B 排在首位
|
||
if (featureData.value.features && featureData.value.features.length > 0) {
|
||
featureData.value.features.sort((a, b) => {
|
||
if (a.api_id === "FLXG0V4B") return -1;
|
||
if (b.api_id === "FLXG0V4B") return 1;
|
||
return 0;
|
||
});
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to fetch product info:', err);
|
||
}
|
||
};
|
||
|
||
// 根据 feature 找到对应的报告类型(保持原来的数据结构匹配方式)
|
||
const findReportTypeByFeature = (feature) => {
|
||
return reportTypes.value.find(type => type.value === feature);
|
||
};
|
||
|
||
// 选择报告类型并设置配置
|
||
const SelectTypePicker = (reportType) => {
|
||
if (!reportType) return;
|
||
|
||
// 保持原来的变量赋值方式
|
||
pickerFieldVal.value = reportType.value;
|
||
pickerFieldText.value = reportType.text;
|
||
selectedReportType.value = [reportType];
|
||
|
||
// 如果产品配置已加载,则设置配置
|
||
if (productConfig.value) {
|
||
// 遍历产品配置,找到匹配的产品(根据 product_en 匹配)
|
||
for (let i of productConfig.value) {
|
||
if (i.product_en === reportType.value) {
|
||
pickerProductConfig.value = i;
|
||
// 新系统:初始价格设置为实际底价(成本价)
|
||
clientPrice.value = Number(i.actual_base_price) || Number(i.price_range_min) || 0;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新路由参数
|
||
router.replace({ query: { ...route.query, feature: reportType.value } });
|
||
|
||
// 重新获取产品信息
|
||
getProductInfo();
|
||
};
|
||
|
||
// 获取产品配置
|
||
const getPromoteConfig = async () => {
|
||
const { data, error } = await getProductConfig();
|
||
|
||
if (data.value && !error.value) {
|
||
if (data.value.code === 200) {
|
||
// 新系统数据结构: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 参数,默认选择第一个报告类型
|
||
let reportType;
|
||
if (currentFeature.value) {
|
||
reportType = findReportTypeByFeature(currentFeature.value);
|
||
}
|
||
|
||
// 如果没有找到匹配的报告类型或没有feature参数,使用第一个报告类型
|
||
if (!reportType && reportTypes.value.length > 0) {
|
||
reportType = reportTypes.value[0];
|
||
}
|
||
|
||
if (reportType) {
|
||
SelectTypePicker(reportType);
|
||
}
|
||
} else {
|
||
console.log("Error fetching product config", data.value);
|
||
}
|
||
}
|
||
};
|
||
|
||
const generatePromotionCode = async () => {
|
||
if (!pickerFieldVal.value || !pickerProductConfig.value) {
|
||
showToast({ message: '请选择报告类型' });
|
||
return;
|
||
}
|
||
|
||
// 确保 price 是有效的数字
|
||
const priceNum = Number(clientPrice.value);
|
||
if (isNaN(priceNum) || priceNum <= 0) {
|
||
showToast({ message: '请输入有效的查询价格' });
|
||
return;
|
||
}
|
||
|
||
// 验证价格范围
|
||
const minPrice = Number(pickerProductConfig.value.price_range_min) || 0;
|
||
const maxPrice = Number(pickerProductConfig.value.price_range_max) || Infinity;
|
||
|
||
if (priceNum < minPrice) {
|
||
showToast({ message: `价格不能低于 ${minPrice.toFixed(2)} 元` });
|
||
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.code === 200) {
|
||
// 使用后端返回的完整短链
|
||
fullLink.value = data.value.data.full_link || "";
|
||
showQRcode.value = true;
|
||
} else {
|
||
console.log("Error generating promotion link", data.value);
|
||
showToast({ message: data.value.msg || '生成推广链接失败,请重试' });
|
||
}
|
||
} else {
|
||
showToast({ message: '生成推广链接失败,请重试' });
|
||
}
|
||
} catch (err) {
|
||
console.error('生成推广链接失败:', err);
|
||
showToast({ message: '生成推广链接失败,请重试' });
|
||
}
|
||
};
|
||
|
||
const onPriceChange = (price) => {
|
||
// 确保接收的价格是数字类型
|
||
const priceNum = Number(price);
|
||
if (!isNaN(priceNum) && isFinite(priceNum)) {
|
||
clientPrice.value = priceNum;
|
||
}
|
||
};
|
||
|
||
// 确认选择报告类型
|
||
const onConfirmType = ({ selectedValues, selectedOptions }) => {
|
||
if (selectedOptions && selectedOptions.length > 0) {
|
||
SelectTypePicker(selectedOptions[0]);
|
||
}
|
||
showTypePicker.value = false;
|
||
};
|
||
|
||
// 跳转到示例报告
|
||
const toExample = () => {
|
||
if (!currentFeature.value) return;
|
||
router.push({ path: '/example', query: { feature: currentFeature.value } });
|
||
};
|
||
|
||
|
||
// 监听路由变化,重新加载数据
|
||
watch(() => route.query.feature, async (newFeature) => {
|
||
if (newFeature) {
|
||
await Promise.all([getPromoteConfig(), getProductInfo()]);
|
||
}
|
||
}, { immediate: false });
|
||
|
||
onMounted(async () => {
|
||
await getPromoteConfig();
|
||
// getProductInfo 会在 SelectTypePicker 中调用
|
||
});
|
||
</script>
|
||
<style scoped>
|
||
.promote {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.promote-header-bg {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 200px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bg-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.card {
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
border-radius: 0.75rem;
|
||
padding: 1.5rem;
|
||
transition: all 0.3s ease;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
box-shadow:
|
||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||
0 2px 4px -1px rgba(0, 0, 0, 0.06),
|
||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.card-with-header {
|
||
position: relative;
|
||
padding-top: 3rem;
|
||
margin-top: -4rem;
|
||
}
|
||
|
||
/* 优雅的报告头部设计 */
|
||
.report-header-elegant {
|
||
position: absolute;
|
||
top: -2.5rem;
|
||
left: 1rem;
|
||
right: 1rem;
|
||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(249, 250, 251, 0.98) 100%);
|
||
border-radius: 1.25rem;
|
||
padding: 1rem 1.5rem;
|
||
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
z-index: 10;
|
||
}
|
||
|
||
.report-header-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.report-logo-container {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.report-logo-wrapper {
|
||
position: relative;
|
||
width: 72px;
|
||
height: 72px;
|
||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 0.15) 100%);
|
||
border-radius: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0.5rem;
|
||
box-shadow:
|
||
0 4px 12px rgba(59, 130, 246, 0.2),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||
}
|
||
|
||
.report-logo-elegant {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||
}
|
||
|
||
.report-info-elegant {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.report-label-elegant {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: #6b7280;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 0.25rem;
|
||
text-transform: uppercase;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.report-title-elegant {
|
||
font-size: 1.375rem;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
line-height: 1.3;
|
||
letter-spacing: -0.01em;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.report-action-elegant {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.4rem;
|
||
padding: 0 0.875rem;
|
||
border-radius: 0.75rem;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
box-shadow: 0 2px 8px rgba(100, 181, 246, 0.25);
|
||
background: linear-gradient(135deg, #64b5f6 0%, #42a5f5 100%);
|
||
border: none;
|
||
height: 36px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.report-action-elegant:active {
|
||
box-shadow: 0 1px 4px rgba(100, 181, 246, 0.2);
|
||
}
|
||
|
||
.report-action-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.report-action-text {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: white;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
/* 精致按钮样式 */
|
||
:deep(.custom-btn-small) {
|
||
border-radius: 0.5rem !important;
|
||
font-weight: 500 !important;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08) !important;
|
||
transition: all 0.3s ease !important;
|
||
padding: 0 16px !important;
|
||
height: 32px !important;
|
||
}
|
||
|
||
:deep(.custom-btn-small:active) {
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) !important;
|
||
}
|
||
|
||
:deep(.custom-btn-primary) {
|
||
border-radius: 0.75rem !important;
|
||
font-weight: 600 !important;
|
||
font-size: 15px !important;
|
||
box-shadow: 0 2px 8px rgba(100, 181, 246, 0.25) !important;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||
background: linear-gradient(135deg, #64b5f6 0%, #42a5f5 100%) !important;
|
||
border: none !important;
|
||
height: 40px !important;
|
||
}
|
||
|
||
:deep(.custom-btn-primary:not(.van-button--disabled):active) {
|
||
box-shadow: 0 1px 4px rgba(100, 181, 246, 0.2) !important;
|
||
}
|
||
|
||
:deep(.custom-btn-primary.van-button--disabled) {
|
||
background: linear-gradient(135deg, #e0e0e0 0%, #bdbdbd 100%) !important;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08) !important;
|
||
opacity: 0.5 !important;
|
||
}
|
||
|
||
:deep(.custom-btn-default) {
|
||
border-radius: 0.75rem !important;
|
||
font-weight: 600 !important;
|
||
font-size: 15px !important;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06) !important;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%) !important;
|
||
border: 1px solid #e0e0e0 !important;
|
||
color: #616161 !important;
|
||
height: 40px !important;
|
||
}
|
||
|
||
:deep(.custom-btn-default:not(.van-button--disabled):active) {
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
|
||
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%) !important;
|
||
}
|
||
|
||
:deep(.custom-btn-default.van-button--disabled) {
|
||
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%) !important;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04) !important;
|
||
opacity: 0.4 !important;
|
||
color: #bdbdbd !important;
|
||
}
|
||
</style>
|