395 lines
12 KiB
Vue
395 lines
12 KiB
Vue
|
|
<script setup>
|
|||
|
|
definePage({
|
|||
|
|
layout: 'PageLayout',
|
|||
|
|
style: {
|
|||
|
|
navigationBarTitleText: '推广报告',
|
|||
|
|
navigationStyle: 'default',
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
|||
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|||
|
|
import useApiFetch from '@/composables/useApiFetch'
|
|||
|
|
import { getProductConfig, generateLink } from '@/api/agent'
|
|||
|
|
import PriceInputPopup from '@/components/PriceInputPopup.vue'
|
|||
|
|
import QRcode from '@/components/QRcode.vue'
|
|||
|
|
import ReportFeatures from '@/components/ReportFeatures.vue'
|
|||
|
|
|
|||
|
|
const currentFeature = ref('')
|
|||
|
|
const reportTypes = ref([])
|
|||
|
|
const showPricePicker = ref(false)
|
|||
|
|
const pickerFieldText = ref('')
|
|||
|
|
const selectedReportTypeIndex = ref(0)
|
|||
|
|
const pickerProductConfig = ref(null)
|
|||
|
|
const pickerFieldVal = ref(null)
|
|||
|
|
const clientPrice = ref(null)
|
|||
|
|
const productConfig = ref(null)
|
|||
|
|
const fullLink = ref('')
|
|||
|
|
const featureData = ref({})
|
|||
|
|
const showQRcode = ref(false)
|
|||
|
|
const showFollowPopup = ref(false)
|
|||
|
|
|
|||
|
|
const logoMap = {
|
|||
|
|
riskassessment: '/static/promote/personal_data_logo.png',
|
|||
|
|
companyinfo: '/static/promote/company_logo.png',
|
|||
|
|
preloanbackgroundcheck: '/static/promote/preloan_background_check_logo.png',
|
|||
|
|
marriage: '/static/promote/marriage_risk_logo.png',
|
|||
|
|
homeservice: '/static/promote/housekeeping_risk_logo.png',
|
|||
|
|
backgroundcheck: '/static/promote/backgroundcheck_logo.png',
|
|||
|
|
consumerFinanceReport: '/static/promote/consumer_finance_report_logo.png',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const currentLogo = computed(() => (currentFeature.value ? logoMap[currentFeature.value] || null : null))
|
|||
|
|
|
|||
|
|
function safeTruncate(num, decimals = 2) {
|
|||
|
|
if (isNaN(num) || !isFinite(num)) return '0.00'
|
|||
|
|
const factor = 10 ** decimals
|
|||
|
|
const scaled = Math.trunc(num * factor)
|
|||
|
|
return (scaled / factor).toFixed(decimals)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const baseCost = computed(() => {
|
|||
|
|
if (!pickerProductConfig.value) return '0.00'
|
|||
|
|
const v = Number(pickerProductConfig.value.actual_base_price) || 0
|
|||
|
|
return safeTruncate(v)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const raiseCost = computed(() => {
|
|||
|
|
if (!pickerProductConfig.value) return '0.00'
|
|||
|
|
const priceNum = Number(clientPrice.value) || 0
|
|||
|
|
const threshold = Number(pickerProductConfig.value.price_threshold) || 0
|
|||
|
|
const rate = Number(pickerProductConfig.value.price_fee_rate) || 0
|
|||
|
|
let cost = 0
|
|||
|
|
if (priceNum > threshold) cost = (priceNum - threshold) * rate
|
|||
|
|
return safeTruncate(cost)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const promotionRevenue = computed(() => {
|
|||
|
|
const priceNum = Number(clientPrice.value) || 0
|
|||
|
|
const base = parseFloat(baseCost.value) || 0
|
|||
|
|
const raise = parseFloat(raiseCost.value) || 0
|
|||
|
|
const revenue = priceNum - base - raise
|
|||
|
|
return safeTruncate(revenue >= 0 ? revenue : 0)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const pickerProductConfigForPopup = computed(() => pickerProductConfig.value || {})
|
|||
|
|
|
|||
|
|
async function getProductInfo() {
|
|||
|
|
if (!currentFeature.value) return
|
|||
|
|
try {
|
|||
|
|
const { data, error } = await useApiFetch(`/product/en/${currentFeature.value}`).get().json()
|
|||
|
|
if (data.value && !error.value && data.value.data) {
|
|||
|
|
featureData.value = data.value.data
|
|||
|
|
if (featureData.value.features?.length) {
|
|||
|
|
featureData.value.features.sort((a, b) => {
|
|||
|
|
if (a.api_id === 'FLXG0V4B') return -1
|
|||
|
|
if (b.api_id === 'FLXG0V4B') return 1
|
|||
|
|
return 0
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findReportTypeByFeature(feature) {
|
|||
|
|
return reportTypes.value.find((t) => t.value === feature)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function selectTypePicker(reportType) {
|
|||
|
|
if (!reportType) return
|
|||
|
|
pickerFieldVal.value = reportType.value
|
|||
|
|
pickerFieldText.value = reportType.text
|
|||
|
|
const idx = reportTypes.value.findIndex((t) => t.value === reportType.value)
|
|||
|
|
selectedReportTypeIndex.value = idx >= 0 ? idx : 0
|
|||
|
|
if (productConfig.value) {
|
|||
|
|
for (const 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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
currentFeature.value = reportType.value
|
|||
|
|
getProductInfo()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function getPromoteConfig() {
|
|||
|
|
const token = typeof uni !== 'undefined' && uni.getStorageSync ? uni.getStorageSync('token') : ''
|
|||
|
|
if (!token) return
|
|||
|
|
const { data, error } = await getProductConfig()
|
|||
|
|
if (!data.value || error.value || data.value.code !== 200) return
|
|||
|
|
const list = data.value.data?.list || []
|
|||
|
|
productConfig.value = list
|
|||
|
|
const types = list.filter((c) => c.product_en).map((c) => ({ text: c.product_name, value: c.product_en, id: c.product_id }))
|
|||
|
|
reportTypes.value = types
|
|||
|
|
let reportType = currentFeature.value ? findReportTypeByFeature(currentFeature.value) : null
|
|||
|
|
if (!reportType && types.length) reportType = types[0]
|
|||
|
|
if (reportType) selectTypePicker(reportType)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onTypePickerChange(e) {
|
|||
|
|
const idx = Number(e.detail?.value ?? e.detail?.value?.[0] ?? 0)
|
|||
|
|
selectedReportTypeIndex.value = idx
|
|||
|
|
const reportType = reportTypes.value[idx]
|
|||
|
|
if (reportType) selectTypePicker(reportType)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function generatePromotionCode() {
|
|||
|
|
if (!pickerFieldVal.value || !pickerProductConfig.value) {
|
|||
|
|
uni.showToast({ title: '请选择报告类型', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const priceNum = Number(clientPrice.value)
|
|||
|
|
if (isNaN(priceNum) || priceNum <= 0) {
|
|||
|
|
uni.showToast({ title: '请输入有效的查询价格', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const minPrice = Number(pickerProductConfig.value.price_range_min) || 0
|
|||
|
|
const maxPrice = Number(pickerProductConfig.value.price_range_max) ?? Infinity
|
|||
|
|
if (priceNum < minPrice) {
|
|||
|
|
uni.showToast({ title: `价格不能低于 ${minPrice.toFixed(2)} 元`, icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (priceNum > maxPrice) {
|
|||
|
|
uni.showToast({ title: `价格不能高于 ${maxPrice.toFixed(2)} 元`, icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
const targetPath = '/agent/promotionInquire/'
|
|||
|
|
const { data, error } = await generateLink({
|
|||
|
|
product_id: pickerProductConfig.value.product_id,
|
|||
|
|
set_price: priceNum,
|
|||
|
|
target_path: targetPath,
|
|||
|
|
})
|
|||
|
|
if (data.value && !error.value && data.value.code === 200) {
|
|||
|
|
fullLink.value = data.value.data?.full_link || ''
|
|||
|
|
showQRcode.value = true
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({ title: data.value?.msg || '生成推广链接失败,请重试', icon: 'none' })
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error(err)
|
|||
|
|
uni.showToast({ title: '生成推广链接失败,请重试', icon: 'none' })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onPriceChange(price) {
|
|||
|
|
const num = Number(price)
|
|||
|
|
if (!isNaN(num) && isFinite(num)) clientPrice.value = num
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openPricePicker() {
|
|||
|
|
showPricePicker.value = true
|
|||
|
|
}
|
|||
|
|
function onUpdateShowPricePicker(v) {
|
|||
|
|
showPricePicker.value = !!v
|
|||
|
|
}
|
|||
|
|
function onUpdateShowQRcode(v) {
|
|||
|
|
showQRcode.value = !!v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toExample() {
|
|||
|
|
if (!currentFeature.value) return
|
|||
|
|
showFollowPopup.value = true
|
|||
|
|
}
|
|||
|
|
function closeFollowPopup() {
|
|||
|
|
showFollowPopup.value = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onLoad((options) => {
|
|||
|
|
currentFeature.value = options?.feature || ''
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
() => currentFeature.value,
|
|||
|
|
() => {
|
|||
|
|
if (currentFeature.value) getPromoteConfig()
|
|||
|
|
},
|
|||
|
|
{ immediate: false },
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
getPromoteConfig()
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<view class="min-h-screen promote bg-gray-100">
|
|||
|
|
<view class="promote-header-bg">
|
|||
|
|
<image src="/static/promote/promote_bg.jpg" class="bg-image" mode="aspectFill" />
|
|||
|
|
</view>
|
|||
|
|
<view class="px-4 pb-6">
|
|||
|
|
<view class="card card-with-header rounded-2xl bg-white shadow-lg p-4 -mt-12 relative">
|
|||
|
|
<view class="report-header-elegant">
|
|||
|
|
<view class="report-header-content">
|
|||
|
|
<view v-if="currentLogo" class="report-logo-wrapper">
|
|||
|
|
<image :src="currentLogo" class="report-logo-elegant" mode="aspectFit" />
|
|||
|
|
</view>
|
|||
|
|
<view class="report-info-elegant">
|
|||
|
|
<view class="report-label-elegant">推广报告</view>
|
|||
|
|
<view class="report-title-elegant">{{ pickerFieldText || '请选择报告类型' }}</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<picker
|
|||
|
|
v-if="reportTypes.length"
|
|||
|
|
mode="selector"
|
|||
|
|
:range="reportTypes"
|
|||
|
|
range-key="text"
|
|||
|
|
:value="selectedReportTypeIndex"
|
|||
|
|
@change="onTypePickerChange"
|
|||
|
|
>
|
|||
|
|
<view class="report-action-elegant">
|
|||
|
|
<text class="report-action-text">切换</text>
|
|||
|
|
</view>
|
|||
|
|
</picker>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="pt-6 space-y-4">
|
|||
|
|
<view class="flex items-center justify-between py-3 border-b border-gray-100">
|
|||
|
|
<text class="font-medium text-gray-700">客户查询价</text>
|
|||
|
|
<text class="text-lg font-semibold text-orange-500">¥{{ clientPrice != null ? clientPrice : '0.00' }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="rounded-xl bg-gray-50 p-3 space-y-2">
|
|||
|
|
<view class="flex justify-between">
|
|||
|
|
<text class="text-sm text-gray-600">底价成本</text>
|
|||
|
|
<text class="text-sm font-semibold text-orange-500">¥{{ baseCost }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="flex justify-between">
|
|||
|
|
<text class="text-sm text-gray-600">提价成本</text>
|
|||
|
|
<text class="text-sm font-semibold text-orange-500">¥{{ raiseCost }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="flex justify-between">
|
|||
|
|
<text class="text-sm text-gray-600">推广收益</text>
|
|||
|
|
<text class="text-sm font-semibold text-orange-500">¥{{ promotionRevenue }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
<view class="grid grid-cols-3 gap-3 mt-4">
|
|||
|
|
<wd-button type="primary" size="large" @click="openPricePicker">设置价格</wd-button>
|
|||
|
|
<wd-button
|
|||
|
|
type="primary"
|
|||
|
|
size="large"
|
|||
|
|
:disabled="!clientPrice || !currentFeature"
|
|||
|
|
@click="generatePromotionCode"
|
|||
|
|
>
|
|||
|
|
推广报告
|
|||
|
|
</wd-button>
|
|||
|
|
<wd-button type="default" size="large" :disabled="!currentFeature" @click="toExample">示例报告</wd-button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view v-if="featureData.product_name" class="card mt-4 rounded-2xl bg-white shadow-lg p-4">
|
|||
|
|
<ReportFeatures :features="featureData.features || []" />
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<PriceInputPopup
|
|||
|
|
:show="showPricePicker"
|
|||
|
|
:default-price="clientPrice"
|
|||
|
|
:product-config="pickerProductConfigForPopup"
|
|||
|
|
@update:show="onUpdateShowPricePicker"
|
|||
|
|
@change="onPriceChange"
|
|||
|
|
/>
|
|||
|
|
<QRcode :show="showQRcode" :full-link="fullLink" @update:show="onUpdateShowQRcode" />
|
|||
|
|
|
|||
|
|
<!-- 示例报告 - 关注公众号弹窗 -->
|
|||
|
|
<view v-if="showFollowPopup" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="closeFollowPopup">
|
|||
|
|
<view class="mx-4 rounded-2xl bg-white p-6 shadow-xl" @click.stop>
|
|||
|
|
<view class="text-center text-lg font-bold text-gray-800 mb-2">查看示例报告</view>
|
|||
|
|
<view class="text-center text-sm text-gray-500 mb-4">请长按识别下方公众号二维码,关注后获取示例报告</view>
|
|||
|
|
<image
|
|||
|
|
src="/static/qrcode/ycc_qrcode.jpg"
|
|||
|
|
class="w-56 h-56 mx-auto block rounded-lg bg-gray-100"
|
|||
|
|
mode="aspectFit"
|
|||
|
|
show-menu-by-longpress
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
class="mt-4 w-full rounded-full py-2.5 text-base font-medium text-white bg-blue-500"
|
|||
|
|
@click="closeFollowPopup"
|
|||
|
|
>
|
|||
|
|
关闭
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.promote-header-bg {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 200rpx;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.bg-image {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
.report-header-elegant {
|
|||
|
|
position: absolute;
|
|||
|
|
top: -2.5rem;
|
|||
|
|
left: 1rem;
|
|||
|
|
right: 1rem;
|
|||
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(249, 250, 251, 0.98));
|
|||
|
|
border-radius: 1.25rem;
|
|||
|
|
padding: 1rem 1.5rem;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
.report-header-content {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 1rem;
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
.report-logo-wrapper {
|
|||
|
|
width: 72rpx;
|
|||
|
|
height: 72rpx;
|
|||
|
|
border-radius: 1rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
.report-logo-elegant {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
.report-info-elegant {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
.report-label-elegant {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #6b7280;
|
|||
|
|
margin-bottom: 4rpx;
|
|||
|
|
}
|
|||
|
|
.report-title-elegant {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #111827;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.report-action-elegant {
|
|||
|
|
padding: 0 14rpx;
|
|||
|
|
height: 72rpx;
|
|||
|
|
border-radius: 12rpx;
|
|||
|
|
background: linear-gradient(135deg, #64b5f6, #42a5f5);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
.report-action-text {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
</style>
|