feat(checkout): 修复后置损耗结算全额归号主、支持角色隔离实时试算与全流程UI重构
- fix(pricing): 后置消耗/损耗金额 100% 划归号主收益,超出预收部分平台不抽成,避免被平台费吸收 - feat(checkout): 前端新增结账实时试算预览组件 (CheckoutTrialPreview + useCheckoutSimulator),角色隔离展示(仅见自身收入/退款,屏蔽平台服务费) - refactor(admin): 重构交易全流程时间线 (ProcessTimeline.vue),将平铺挤压指标重构成4大分类仪表盘微型卡片矩阵,升级可视化业务状态流转徽章与阶段色彩主题 - style(admin): 优化支付与交接流水元信息网格布局,防止超宽屏下长单行拉伸
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user