186 lines
6.3 KiB
Vue
186 lines
6.3 KiB
Vue
<template>
|
||
<van-popup v-model:show="show" destroy-on-close round position="bottom">
|
||
<div class="min-h-[500px] bg-gray-50 text-gray-600">
|
||
<div class="h-10 bg-white flex items-center justify-center font-semibold text-lg">设置客户查询价
|
||
</div>
|
||
<div class="card m-4">
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-lg">
|
||
客户查询价 (元)</div>
|
||
</div>
|
||
|
||
<div class="border-b border-gray-200">
|
||
<van-field v-model="price" type="number" label="¥" label-width="28"
|
||
:placeholder="`${productConfig.price_range_min} - ${productConfig.price_range_max}`"
|
||
@blur="onBlurPrice" class="!text-3xl" />
|
||
</div>
|
||
<div class="flex items-center justify-between mt-2">
|
||
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span>元</div>
|
||
</div>
|
||
<div class="flex items-center justify-between mt-2">
|
||
<div>底价成本为<span class="text-orange-500"> {{ baseCost }} </span>元</div>
|
||
<div>提价成本为<span class="text-orange-500"> {{ raiseCost }} </span>元</div>
|
||
</div>
|
||
</div>
|
||
<div class="card m-4">
|
||
<div class="text-lg mb-2">收益与成本说明</div>
|
||
|
||
<div>推广收益 = 客户查询价 - 我的成本</div>
|
||
<div>我的成本 = 实际底价 + 提价成本</div>
|
||
<div class="mt-1">提价成本:设置{{ productConfig.price_threshold }}元以上的部分收取{{
|
||
rateFormat(productConfig.price_fee_rate)
|
||
}}的提价成本</div>
|
||
<div class="">设定范围:<span class="text-orange-500">{{
|
||
productConfig.price_range_min }}</span>元 - <span class="text-orange-500">{{
|
||
productConfig.price_range_max }}</span>元</div>
|
||
</div>
|
||
<div class="px-4 pb-4">
|
||
<van-button class="w-full" round type="primary" size="large" @click="onConfirm">确认</van-button>
|
||
</div>
|
||
</div>
|
||
</van-popup>
|
||
</template>
|
||
|
||
<script setup>
|
||
const props = defineProps({
|
||
defaultPrice: {
|
||
type: Number,
|
||
required: true
|
||
},
|
||
productConfig: {
|
||
type: Object,
|
||
required: true
|
||
}
|
||
})
|
||
const { defaultPrice, productConfig } = toRefs(props)
|
||
const emit = defineEmits(["change"])
|
||
const show = defineModel("show")
|
||
const price = ref(null)
|
||
|
||
|
||
watch(show, () => {
|
||
price.value = defaultPrice.value
|
||
})
|
||
|
||
|
||
const costPrice = computed(() => {
|
||
if (!productConfig.value) return 0.00
|
||
|
||
// 新代理系统:成本价 = 实际底价(actual_base_price)+ 提价成本
|
||
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
|
||
const priceNum = Number(price.value) || 0;
|
||
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
|
||
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
|
||
|
||
// 计算提价成本
|
||
let priceCost = 0;
|
||
if (priceNum > priceThreshold) {
|
||
priceCost = (priceNum - priceThreshold) * priceFeeRate;
|
||
}
|
||
|
||
// 总成本 = 实际底价 + 提价成本
|
||
const totalCost = actualBasePrice + priceCost;
|
||
|
||
return safeTruncate(totalCost);
|
||
})
|
||
const rateFormat = (rate) => {
|
||
return rate * 100 + '%';
|
||
}
|
||
const baseCost = computed(() => {
|
||
if (!productConfig.value) return "0.00";
|
||
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
|
||
return safeTruncate(actualBasePrice);
|
||
})
|
||
|
||
const raiseCost = computed(() => {
|
||
if (!productConfig.value) return "0.00";
|
||
const priceNum = Number(price.value) || 0;
|
||
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
|
||
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
|
||
let priceCost = 0;
|
||
if (priceNum > priceThreshold) {
|
||
priceCost = (priceNum - priceThreshold) * priceFeeRate;
|
||
}
|
||
return safeTruncate(priceCost);
|
||
})
|
||
|
||
const promotionRevenue = computed(() => {
|
||
return safeTruncate(price.value - costPrice.value)
|
||
});
|
||
|
||
// 价格校验与修正逻辑
|
||
const validatePrice = (currentPrice) => {
|
||
const min = productConfig.value.price_range_min;
|
||
const max = productConfig.value.price_range_max;
|
||
let newPrice = Number(currentPrice);
|
||
let message = '';
|
||
|
||
// 处理无效输入
|
||
if (isNaN(newPrice)) {
|
||
newPrice = defaultPrice.value;
|
||
return { newPrice, message: '输入无效,请输入价格' };
|
||
}
|
||
|
||
// 处理小数位数(兼容科学计数法)
|
||
try {
|
||
const priceString = newPrice.toString()
|
||
const [_, decimalPart = ""] = priceString.split('.');
|
||
console.log(priceString, decimalPart)
|
||
// 当小数位数超过2位时处理
|
||
if (decimalPart.length > 2) {
|
||
newPrice = parseFloat(safeTruncate(newPrice));
|
||
message = '价格已自动格式化为两位小数';
|
||
}
|
||
} catch (e) {
|
||
console.error('价格格式化异常:', e);
|
||
}
|
||
|
||
// 范围校验(基于可能格式化后的值)
|
||
if (newPrice < min) {
|
||
message = `价格不能低于 ${min} 元`;
|
||
newPrice = min;
|
||
} else if (newPrice > max) {
|
||
message = `价格不能高于 ${max} 元`;
|
||
newPrice = max;
|
||
}
|
||
console.log(newPrice, message)
|
||
return { newPrice, message };
|
||
}
|
||
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 isManualConfirm = ref(false)
|
||
const onConfirm = () => {
|
||
if (isManualConfirm.value) return
|
||
const { newPrice, message } = validatePrice(price.value)
|
||
if (message) {
|
||
price.value = newPrice
|
||
showToast({ message });
|
||
} else {
|
||
emit("change", price.value)
|
||
show.value = false
|
||
}
|
||
|
||
|
||
}
|
||
const onBlurPrice = () => {
|
||
const { newPrice, message } = validatePrice(price.value)
|
||
if (message) {
|
||
isManualConfirm.value = true
|
||
price.value = newPrice
|
||
showToast({ message });
|
||
}
|
||
setTimeout(() => {
|
||
isManualConfirm.value = false
|
||
}, 0)
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped></style>
|