Files
hfb_sys/frontend/src/features/orders/composables/useOrderSnapshot.ts
T

311 lines
9.8 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
otherAmountYuan?: 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(snapshotHafCoinM.value - Number(options.checkoutForm.value.coin_consumed_m || 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,
depositDeductAmountYuan: options.checkoutForm.value.otherAmountYuan,
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
depositDeductAmountYuan?: 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) {
const remain = Number(options.remainingHafCoinM || 0)
if (remain >= 0) {
lines.push(
`哈夫币实际消耗:${quantity(options.coinConsumedM)}M,预计剩余${quantity(remain)}M`
)
} else {
lines.push(
`哈夫币实际消耗:${quantity(options.coinConsumedM)}M,打超${quantity(-remain)}M(超额按比例从押金结算)`
)
}
}
const depositDeductAmountYuan = Number(options.depositDeductAmountYuan || 0)
if (depositDeductAmountYuan > 0) {
lines.push(`押金赔付扣除:¥${depositDeductAmountYuan.toFixed(2)}(将直接从预收押金中扣除)`)
}
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: '卖家交接',
platform_handoff: '客服代交接',
renter_checkout: '买家结账',
owner_counter_checkout: '卖家反驳结账',
platform_checkout_counter: '客服修改结账',
renter_confirm_checkout: '买家确认结账',
owner_accept_checkout: '卖家接受结账',
admin_arbitration: '客服仲裁',
dispute_opened: '发起申诉',
checkout_dispute_opened: '发起结账争议',
platform_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
}