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 checkoutForm: Ref resourceUsage: Ref> }) { 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 ) { const next: Record = {} 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, 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 ) { 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 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 = { 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 { return typeof value === 'object' && value !== null }