Files
hfb_sys/frontend/src/features/orders/composables/useCheckoutSimulator.ts
T
yml2213 dff0577385 feat(checkout): 修复后置损耗结算全额归号主、支持角色隔离实时试算与全流程UI重构
- fix(pricing): 后置消耗/损耗金额 100% 划归号主收益,超出预收部分平台不抽成,避免被平台费吸收
- feat(checkout): 前端新增结账实时试算预览组件 (CheckoutTrialPreview + useCheckoutSimulator),角色隔离展示(仅见自身收入/退款,屏蔽平台服务费)
- refactor(admin): 重构交易全流程时间线 (ProcessTimeline.vue),将平铺挤压指标重构成4大分类仪表盘微型卡片矩阵,升级可视化业务状态流转徽章与阶段色彩主题
- style(admin): 优化支付与交接流水元信息网格布局,防止超宽屏下长单行拉伸
2026-09-13 10:19:33 +08:00

170 lines
6.3 KiB
TypeScript

import { computed, type Ref } from 'vue'
import type { Order } from '../api/orders'
import type { CounterFormState } from './useOrderActions'
import { readSnapshot, roundMoney, roundQuantity, readNumber } from './useOrderSnapshot'
export interface OwnerSimulation {
role: 'owner'
totalIncomeYuan: number
rentIncomeYuan: number
damageCompensationYuan: number
lossCompensationYuan: number
shortfallYuan: number
isDepositSufficient: boolean
}
export interface RenterSimulation {
role: 'renter'
actualCostYuan: number
buyerRentYuan: number
damageDeductYuan: number
totalRefundYuan: number
rentRefundYuan: number
depositRefundYuan: number
overshootYuan: number
shortfallYuan: number
isDepositSufficient: boolean
}
export type CheckoutSimulation = OwnerSimulation | RenterSimulation | null
export function useCheckoutSimulator(options: {
order: Ref<Order | null>
isOwner: Ref<boolean>
isRenter: Ref<boolean>
form: Ref<CounterFormState>
}) {
return computed<CheckoutSimulation>(() => {
const o = options.order.value
if (!o) return null
const consumedM = roundQuantity(Math.max(Number(options.form.value.coin_consumed_m || 0), 0))
const consumableYuan = roundMoney(
Math.max(Number(options.form.value.consumableAmountYuan || 0), 0)
)
const depositDeductYuan = roundMoney(
Math.max(Number(options.form.value.depositDeductAmountYuan || 0), 0)
)
const snapshot = readSnapshot(o)
const assetSummary = snapshot?.asset_summary as Record<string, unknown> | undefined
const priceBreakdown =
((assetSummary?.price_breakdown || snapshot?.price_breakdown) as
| Record<string, unknown>
| undefined) || {}
const totalCoinM = roundQuantity(readNumber(snapshot?.haf_coin_amount) / 1000000)
const buyerRatio = readNumber(priceBreakdown['buyer_ratio'])
const sellerRatio = readNumber(priceBreakdown['seller_ratio'])
const buyerCoinBaseYuan = readNumber(priceBreakdown['buyer_coin_base_price'])
const sellerCoinBaseYuan = readNumber(priceBreakdown['seller_coin_base_price'])
const prepaidConsumableYuan = readNumber(priceBreakdown['consumable_price'])
const coinUseRatio = totalCoinM > 0 ? roundQuantity(consumedM / totalCoinM) : 1
const isCoinOvershoot = totalCoinM > 0 && consumedM > totalCoinM
const depositYuan = Number(o.deposit_amount_cent || 0) / 100
if (options.isOwner.value) {
// ────────── 号主视角试算 ──────────
let ownerCoinYuan =
sellerRatio > 0 && (totalCoinM > 0 || consumedM > 0)
? roundMoney((consumedM * 100) / sellerRatio)
: roundMoney(sellerCoinBaseYuan * coinUseRatio)
if (!isCoinOvershoot) {
ownerCoinYuan = Math.min(
ownerCoinYuan,
sellerCoinBaseYuan > 0 ? sellerCoinBaseYuan : (o.owner_rent_amount_cent || 0) / 100
)
}
// 消耗品/后置损耗计算
let ownerConsumableYuan: number
if (prepaidConsumableYuan > 0) {
const ownerRentBaseYuan = (o.owner_rent_amount_cent || 0) / 100
const prepaidOwnerConsumableYuan = Math.max(
roundMoney(ownerRentBaseYuan - sellerCoinBaseYuan),
0
)
if (consumableYuan <= prepaidConsumableYuan) {
ownerConsumableYuan = roundMoney(
prepaidOwnerConsumableYuan * (consumableYuan / prepaidConsumableYuan)
)
} else {
// 超出预收部分全额归号主
const excessYuan = roundMoney(consumableYuan - prepaidConsumableYuan)
ownerConsumableYuan = roundMoney(prepaidOwnerConsumableYuan + excessYuan)
}
} else {
// 无预收消耗品:后置申报损耗 100% 归号主
ownerConsumableYuan = consumableYuan
}
const ownerRentIncomeYuan = roundMoney(ownerCoinYuan + ownerConsumableYuan)
// 押金赔付覆盖与差额
const damageCoveredYuan = Math.min(depositDeductYuan, depositYuan)
const shortfallYuan = Math.max(roundMoney(depositDeductYuan - depositYuan), 0)
const totalIncomeYuan = roundMoney(ownerRentIncomeYuan + damageCoveredYuan)
return {
role: 'owner',
totalIncomeYuan,
rentIncomeYuan: ownerRentIncomeYuan,
damageCompensationYuan: damageCoveredYuan,
lossCompensationYuan: ownerConsumableYuan,
shortfallYuan,
isDepositSufficient: shortfallYuan <= 0,
}
}
if (options.isRenter.value) {
// ────────── 租客视角试算 ──────────
let buyerCoinYuan =
buyerRatio > 0 && (totalCoinM > 0 || consumedM > 0)
? roundMoney((consumedM * 100) / buyerRatio)
: roundMoney(buyerCoinBaseYuan * coinUseRatio)
if (!isCoinOvershoot) {
buyerCoinYuan = Math.min(
buyerCoinYuan,
buyerCoinBaseYuan > 0 ? buyerCoinBaseYuan : (o.rent_amount_cent || 0) / 100
)
}
const actualBuyerRentYuan = roundMoney(buyerCoinYuan + consumableYuan)
const prepaidRentYuan = (o.rent_amount_cent || 0) / 100
const rentOvershootYuan = Math.max(roundMoney(actualBuyerRentYuan - prepaidRentYuan), 0)
const rentRefundYuan = Math.max(roundMoney(prepaidRentYuan - actualBuyerRentYuan), 0)
const damageAppliedYuan = Math.min(depositDeductYuan, depositYuan)
const depositLeftAfterDamage = Math.max(roundMoney(depositYuan - damageAppliedYuan), 0)
const overshootCoveredYuan = Math.min(rentOvershootYuan, depositLeftAfterDamage)
const depositRefundYuan = Math.max(
roundMoney(depositLeftAfterDamage - overshootCoveredYuan),
0
)
const shortfallYuan = roundMoney(
Math.max(roundMoney(depositDeductYuan - damageAppliedYuan), 0) +
Math.max(roundMoney(rentOvershootYuan - overshootCoveredYuan), 0)
)
const totalRefundYuan = roundMoney(rentRefundYuan + depositRefundYuan)
const actualCostYuan = roundMoney(actualBuyerRentYuan + damageAppliedYuan)
return {
role: 'renter',
actualCostYuan,
buyerRentYuan: actualBuyerRentYuan,
damageDeductYuan: depositDeductYuan,
totalRefundYuan,
rentRefundYuan,
depositRefundYuan,
overshootYuan: rentOvershootYuan,
shortfallYuan,
isDepositSufficient: shortfallYuan <= 0,
}
}
return null
})
}