Files
uniapp_ycc/src/pages/promote/report.vue
2026-02-09 15:13:40 +08:00

395 lines
12 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.

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