Files
hfb_sys/frontend/src/features/orders/composables/useOrderSnapshot.ts
T
yml2213 98772867a7 统一结账押金字段为押金赔付扣除
号主修正去掉无效的其他押金扣款,仅保留押金赔付扣除;
前后端与账单文案统一,修正时 other 与 deduct 对齐。
2026-07-17 17:17:29 +08:00

297 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { computed, type Ref } from 'vue'
import { centToYuan, formatMoney } from '@/shared/utils/money'
import type { Checkout, Order } from '../api/orders'
export interface CheckoutFormSnapshot {
content: string
coin_consumed_m: number
}
export interface CounterFormSnapshot {
consumableAmountYuan: number
coin_consumed_m: number
depositDeductAmountYuan: number
reason: string
evidenceText: string
}
export interface CheckoutResource {
key: string
label: string
price: string
mode: string
quantity: number
unitPrice: number
}
export function useOrderCheckoutSnapshot(options: {
order: Ref<Order | null>
checkoutForm: Ref<CheckoutFormSnapshot>
resourceUsage: Ref<Record<string, number>>
}) {
const checkoutResources = computed(() => readSnapshotResources(options.order.value))
const resourceChargeAmount = computed(() =>
calculateResourceChargeAmount(checkoutResources.value, options.resourceUsage.value)
)
const snapshotHafCoinM = computed(() => getSnapshotHafCoinM(options.order.value))
const remainingHafCoinM = computed(() =>
roundQuantity(
Math.max(snapshotHafCoinM.value - Number(options.checkoutForm.value.coin_consumed_m || 0), 0)
)
)
function hydrateResourceUsage() {
options.resourceUsage.value = normalizeResourceUsage(
checkoutResources.value,
options.resourceUsage.value
)
}
function readResourceUsage(key: string) {
return readResourceUsageAmount(options.resourceUsage.value, key)
}
function resourceLineAmount(item: CheckoutResource) {
return roundMoney(readResourceUsage(item.key) * item.unitPrice)
}
function checkoutContentWithSummary() {
return buildCheckoutContentWithSummary({
content: options.checkoutForm.value.content,
coinConsumedM: options.checkoutForm.value.coin_consumed_m,
checkoutResources: checkoutResources.value,
resourceUsage: options.resourceUsage.value,
remainingHafCoinM: remainingHafCoinM.value,
})
}
return {
checkoutResources,
resourceChargeAmount,
snapshotHafCoinM,
remainingHafCoinM,
hydrateResourceUsage,
readResourceUsage,
isChargedResource,
resourceLineAmount,
checkoutContentWithSummary,
}
}
export function readSnapshot(order: Order | null) {
const snapshot = order?.account_snapshot
return isRecord(snapshot) ? snapshot : null
}
export function readAssetSummary(order: Order | null) {
const summary = readSnapshot(order)?.asset_summary
return isRecord(summary) ? summary : null
}
export function readSnapshotResources(order: Order | null): CheckoutResource[] {
const resources = readAssetSummary(order)?.resources
if (!Array.isArray(resources)) return []
return resources
.filter(isRecord)
.map(item => {
const key = String(item.key || item.label || '')
const label = String(item.label || key || '额外消耗品')
const price = String(item.price || '')
return {
key,
label,
price,
mode: String(item.mode || '收费'),
quantity: readNumber(item.quantity),
unitPrice: readUnitPrice(price),
}
})
.filter(item => item.key && item.quantity > 0)
}
export function normalizeResourceUsage(
resources: CheckoutResource[],
current: Record<string, number>
) {
const next: Record<string, number> = {}
for (const item of resources) {
next[item.key] = Math.min(Math.max(Number(current[item.key] || 0), 0), item.quantity)
}
return next
}
export function readResourceUsageAmount(resourceUsage: Record<string, number>, key: string) {
return Math.max(Number(resourceUsage[key] || 0), 0)
}
export function isChargedResource(item: CheckoutResource) {
return item.mode !== '赠送'
}
export function calculateResourceChargeAmount(
resources: CheckoutResource[],
resourceUsage: Record<string, number>
) {
return roundMoney(
resources.reduce((sum, item) => {
if (!isChargedResource(item)) return sum
const used = readResourceUsageAmount(resourceUsage, item.key)
return sum + used * item.unitPrice
}, 0)
)
}
export function buildCheckoutContentWithSummary(options: {
content: string
coinConsumedM: number
checkoutResources: CheckoutResource[]
resourceUsage: Record<string, number>
remainingHafCoinM: number
}) {
const lines = [options.content.trim()].filter(Boolean)
const usedResources = options.checkoutResources.filter(
item => readResourceUsageAmount(options.resourceUsage, item.key) > 0
)
if (usedResources.length) {
lines.push(
`额外消耗品:${usedResources
.map(item => {
const used = readResourceUsageAmount(options.resourceUsage, item.key)
const amount = roundMoney(used * item.unitPrice)
return `${item.label} ${used}/${item.quantity}${
isChargedResource(item) ? `,金额¥${money(amount)}` : ',赠送不扣款'
}`
})
.join(';')}`
)
}
if (Number(options.coinConsumedM || 0) > 0) {
lines.push(
`哈夫币消耗:${quantity(options.coinConsumedM)}M,预计剩余${quantity(
options.remainingHafCoinM
)}M`
)
}
if (lines.length === 0) {
lines.push('租客发起结账。')
}
return lines.join('\n')
}
export function hydrateCounterFormFromCheckout(checkout: Checkout): CounterFormSnapshot {
// 号主修正只暴露押金赔付扣除;优先用结算字段,兼容旧单 only other 的情况
const depositDeductYuan = amountYuan(
checkout.deposit_deduct_amount_cent > 0
? checkout.deposit_deduct_amount_cent
: checkout.other_amount_cent
)
return {
consumableAmountYuan: amountYuan(checkout.consumable_amount_cent),
coin_consumed_m: checkout.coin_consumed_m,
depositDeductAmountYuan: depositDeductYuan,
reason: '',
evidenceText: '',
}
}
export function calculateCounterTotal(counterForm: {
consumableAmountYuan: number
depositDeductAmountYuan: number
}) {
return roundMoney(counterForm.consumableAmountYuan + counterForm.depositDeductAmountYuan)
}
export function getSnapshotHafCoinM(order: Order | null) {
const snapshot = readSnapshot(order)
return roundQuantity(readNumber(snapshot?.haf_coin_amount) / 1000000)
}
export function orderEstimatedEndAt(order: Order | null) {
if (order?.estimated_end_at) return order.estimated_end_at
const rentedAt = order?.rented_at
const durationHours = Number(order?.estimated_duration_hours || 0)
if (!rentedAt || durationHours <= 0) return undefined
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
}
export function orderRentAmount(item: Order, userID: number | undefined | null) {
if (item.owner_id === userID) return amountYuan(item.owner_rent_amount_cent)
if (item.renter_id === userID) return amountYuan(item.rent_amount_cent)
return amountYuan(item.display_amount_cent)
}
export function ownerActualIncome(item: Order, userID: number | undefined | null) {
if (item.owner_id !== userID) return null
const value = item.checkout?.owner_income_amount_cent
return typeof value === 'number' ? centToYuan(value) : null
}
export function formatHandoffRecordType(type: string) {
const typeMap: Record<string, string> = {
owner_handoff: '卖家交接',
renter_checkout: '买家结账',
owner_counter_checkout: '卖家反驳结账',
renter_confirm_checkout: '买家确认结账',
owner_accept_checkout: '卖家接受结账',
admin_arbitration: '客服仲裁',
dispute_opened: '发起申诉',
checkout_dispute_opened: '发起结账争议',
dispute_cancelled: '取消申诉',
checkout_dispute_cancelled: '取消结账争议',
}
return typeMap[type] || type
}
export function linesToList(value: string) {
return value
.split('\n')
.map(item => item.trim())
.filter(Boolean)
}
export function money(value: unknown) {
return formatMoney(readNumber(value))
}
export function amountYuan(cent: unknown) {
if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent))
return 0
}
export function quantity(value: unknown) {
const rounded = roundQuantity(readNumber(value))
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2)
}
export function readUnitPrice(priceText: string) {
const normalized = priceText.replace(/,/g, ',').trim()
const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/)
if (fractionMatch) {
const amount = Number(fractionMatch[1])
const count = Number(fractionMatch[2])
return count > 0 ? roundMoney(amount / count) : 0
}
const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/)
if (singleMatch) return Number(singleMatch[1])
const fallback = normalized.match(/(\d+(?:\.\d+)?)/)
return fallback ? Number(fallback[1]) : 0
}
export function roundMoney(value: number) {
return Math.round(Number(value || 0) * 10) / 10
}
export function roundQuantity(value: number) {
return Math.round(value * 100) / 100
}
export function readNumber(value: unknown) {
const number = Number(value || 0)
return Number.isFinite(number) ? number : 0
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}