This commit is contained in:
Mrx
2026-02-09 15:13:40 +08:00
commit e84094946a
123 changed files with 23042 additions and 0 deletions

View File

@@ -0,0 +1,394 @@
<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>