f
This commit is contained in:
394
src/pages/promote/report.vue
Normal file
394
src/pages/promote/report.vue
Normal 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>
|
||||
Reference in New Issue
Block a user