Files
ycc-proxy-webview/src/views/Promote.vue
2025-12-16 19:27:20 +08:00

605 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>