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