diff --git a/src/components/EmptyState.vue b/src/components/EmptyState.vue new file mode 100644 index 0000000..1b60180 --- /dev/null +++ b/src/components/EmptyState.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/PriceInputPopup.vue b/src/components/PriceInputPopup.vue index cd8127e..23a761f 100644 --- a/src/components/PriceInputPopup.vue +++ b/src/components/PriceInputPopup.vue @@ -15,11 +15,11 @@ @blur="onBlurPrice" class="!text-3xl" />
-
推广收益为 {{ promotionRevenue }}
+
推广收益为 {{ pricingResult.promotionRevenue }}
-
底价成本为 {{ baseCost }}
-
提价成本为 {{ raiseCost }}
+
底价成本为 {{ pricingResult.baseCost }}
+
提价成本为 {{ pricingResult.raiseCost }}
@@ -42,6 +42,8 @@ diff --git a/src/utils/promotionPricing.js b/src/utils/promotionPricing.js new file mode 100644 index 0000000..57c4dd4 --- /dev/null +++ b/src/utils/promotionPricing.js @@ -0,0 +1,162 @@ +/** + * 推广定价计算工具 + * 统一管理价格计算逻辑,供 Promote.vue 和 PriceInputPopup.vue 共享 + */ + +/** + * 安全截断小数位数(四舍五入) + * 用于前端显示和发送给后端的价格格式化 + * @param {number} num - 要格式化的数值 + * @param {number} decimals - 保留小数位数,默认2位 + * @returns {string} 格式化后的字符串,如 "12.34" + */ +export function safeTruncate(num, decimals = 2) { + if (Number.isNaN(num) || !Number.isFinite(num)) { + return '0.00' + } + + const factor = 10 ** decimals + const scaled = Math.round(num * factor) + return (scaled / factor).toFixed(decimals) +} + +/** + * 将元转为分(整数),避免浮点运算精度问题 + * @param {number} num - 元为单位的数值 + * @returns {number} 分为单位的整数 + */ +function toTruncatedCents(num) { + if (Number.isNaN(num) || !Number.isFinite(num)) { + return 0 + } + return Math.round(num * 100) +} + +/** + * 将分转回元,返回固定2位小数字符串 + * @param {number} cents - 分为单位的整数 + * @returns {string} 元为单位的字符串,如 "12.34" + */ +function centsToFixed(cents) { + return (cents / 100).toFixed(2) +} + +/** + * 计算提价成本 + * 当客户查询价超过价格阈值时,超出部分按费率收取 + * @param {number} price - 客户查询价 + * @param {object} config - 产品配置 + * @returns {number} 提价成本(元) + */ +function calculateRaiseCost(price, config) { + const priceThreshold = Number(config.price_threshold) || 0 + const priceFeeRate = Number(config.price_fee_rate) || 0 + + if (priceThreshold <= 0 || priceFeeRate <= 0) { + return 0 + } + + if (price <= priceThreshold) { + return 0 + } + + return (price - priceThreshold) * priceFeeRate +} + +/** + * 计算推广定价(核心函数) + * 所有金额先转为分(整数)再计算,避免浮点精度问题 + * + * @param {number|string} priceInput - 客户查询价 + * @param {object} config - 产品配置,包含以下字段: + * - actual_base_price: 实际底价(含等级加成) + * - price_threshold: 价格阈值(超过此价格收取提价费用) + * - price_fee_rate: 提价费率(0-1之间的小数) + * @returns {{ costPrice: string, baseCost: string, raiseCost: string, promotionRevenue: string }} + * 各项价格均为 "xx.xx" 格式的字符串 + */ +export function calculatePromotionPricing(priceInput, config) { + if (!config) { + return { + costPrice: '0.00', + baseCost: '0.00', + raiseCost: '0.00', + promotionRevenue: '0.00', + } + } + + const price = Number(priceInput) + if (!Number.isFinite(price)) { + return { + costPrice: '0.00', + baseCost: '0.00', + raiseCost: '0.00', + promotionRevenue: '0.00', + } + } + + const baseCost = Number(config.actual_base_price) || 0 + const raiseCost = calculateRaiseCost(price, config) + const totalCost = baseCost + raiseCost + + // 转为分计算,避免浮点精度问题 + const priceCents = toTruncatedCents(price) + const baseCostCents = toTruncatedCents(baseCost) + const raiseCostCents = toTruncatedCents(raiseCost) + const totalCostCents = toTruncatedCents(totalCost) + const revenueCents = Math.max(0, priceCents - totalCostCents) + + return { + costPrice: centsToFixed(totalCostCents), + baseCost: centsToFixed(baseCostCents), + raiseCost: centsToFixed(raiseCostCents), + promotionRevenue: centsToFixed(revenueCents), + } +} + +/** + * 验证价格输入 + * @param {number|string} currentPrice - 当前输入的价格 + * @param {object} productConfig - 产品配置 + * @param {number} defaultPrice - 默认价格(用于无效输入时的回退) + * @returns {{ newPrice: number, message: string }} + */ +export function validatePrice(currentPrice, productConfig, defaultPrice) { + if (!productConfig) { + return { newPrice: Number(defaultPrice || 0), message: '产品配置未就绪,请稍后再试' } + } + + const min = Number(productConfig.price_range_min) || 0 + const max = Number(productConfig.price_range_max) || Infinity + let newPrice = Number(currentPrice) + let message = '' + + // 处理无效输入 + if (Number.isNaN(newPrice)) { + newPrice = Number(defaultPrice || 0) + return { newPrice, message: '输入无效,请输入价格' } + } + + // 处理小数位数(兼容科学计数法) + try { + const priceString = newPrice.toString() + const [_, decimalPart = ''] = priceString.split('.') + if (decimalPart.length > 2) { + newPrice = Number.parseFloat(safeTruncate(newPrice)) + message = '价格已自动格式化为两位小数' + } + } catch (e) { + console.error('价格格式化异常:', e) + } + + // 范围校验 + if (newPrice < min) { + message = `价格不能低于 ${min} 元` + newPrice = min + } else if (newPrice > max) { + message = `价格不能高于 ${max} 元` + newPrice = max + } + + return { newPrice, message } +} diff --git a/src/views/AgentPromoteDetails.vue b/src/views/AgentPromoteDetails.vue index 661cc5d..17acb27 100644 --- a/src/views/AgentPromoteDetails.vue +++ b/src/views/AgentPromoteDetails.vue @@ -1,7 +1,7 @@ diff --git a/src/views/Withdraw.vue b/src/views/Withdraw.vue index 23fad58..454d95a 100644 --- a/src/views/Withdraw.vue +++ b/src/views/Withdraw.vue @@ -13,105 +13,123 @@ 根据相关规定,提现功能需要完成实名认证后才能使用,提现金额将转入您实名认证的账户中。

+ style="background-color: var(--van-theme-primary);" @click="openRealNameAuth"> 立即实名认证
-
+
- - -
-
- - 支付宝提现 -
- -
- - - - - 可填写支付宝账户绑定的手机号 -
- -
- - - - - 请填写支付宝账户认证的真实姓名 -
+
+
+
+
+ + 支付宝
- - -
-
- - 银行卡提现 -
- -
- - - - - 请填写与实名一致的开户银行卡号 -
- -
- - - - -
- -
- - - - - 需与银行卡开户姓名一致 -
+
+ + 银行卡
- - +
+
+ + +
+
+ + 支付宝提现 +
+ +
+ + + + + 可填写支付宝账户绑定的手机号 +
+ +
+ + + + + 请填写支付宝账户认证的真实姓名 +
+
+ + +
+
+ + 银行卡提现 +
+ +
+ + + + + 请填写与实名一致的开户银行卡号 +
+ +
+ + + + +
+ +
+ + + + + 需与银行卡开户姓名一致 +
+
@@ -125,8 +143,7 @@