按订单快照计算结账消耗金额

This commit is contained in:
yml
2026-05-24 20:05:06 +08:00
parent cd345d574a
commit b298c95a5c
+206 -6
View File
@@ -48,6 +48,7 @@ const checkoutForm = ref({
other_amount: 0,
evidenceText: '',
})
const resourceUsage = ref<Record<string, number>>({})
const counterForm = ref({
consumable_amount: 0,
coin_consumed_m: 0,
@@ -70,6 +71,26 @@ const canOpenDispute = computed(() => {
const isCheckoutDisputeStage = computed(() => {
return !!order.value && ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.value.status)
})
const checkoutResources = computed(() => {
const resources = readSnapshotResources()
return resources.filter((item) => item.quantity > 0)
})
const resourceChargeAmount = computed(() => {
return roundMoney(
checkoutResources.value.reduce((sum, item) => {
if (!isChargedResource(item)) return sum
const used = readResourceUsage(item.key)
return sum + used * item.unitPrice
}, 0),
)
})
const snapshotHafCoinM = computed(() => {
const snapshot = readSnapshot()
return roundMoney(readNumber(snapshot?.haf_coin_amount) / 1000000)
})
const remainingHafCoinM = computed(() => {
return roundMoney(Math.max(snapshotHafCoinM.value - Number(checkoutForm.value.coin_consumed_m || 0), 0))
})
onMounted(loadOrder)
@@ -78,6 +99,7 @@ async function loadOrder() {
try {
order.value = await fetchOrder(String(route.params.id))
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
hydrateResourceUsage()
hydrateCounterForm()
} finally {
loading.value = false
@@ -145,9 +167,11 @@ async function handleSubmitCheckout() {
if (!order.value) return
returning.value = true
try {
const consumableAmount = resourceChargeAmount.value
checkoutForm.value.consumable_amount = consumableAmount
await submitCheckout(order.value.id, {
content: checkoutForm.value.content,
consumable_amount: checkoutForm.value.consumable_amount,
content: checkoutContentWithSummary(),
consumable_amount: consumableAmount,
coin_consumed_m: checkoutForm.value.coin_consumed_m,
other_amount: checkoutForm.value.other_amount,
evidence_urls: linesToList(checkoutForm.value.evidenceText),
@@ -159,6 +183,7 @@ async function handleSubmitCheckout() {
other_amount: 0,
evidenceText: '',
}
resourceUsage.value = {}
ElMessage.success('结账已发起,等待号主确认')
await loadOrder()
} catch (error) {
@@ -295,6 +320,114 @@ function orderEstimatedEndAt() {
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
}
interface CheckoutResource {
key: string
label: string
price: string
mode: string
quantity: number
unitPrice: number
}
function readSnapshot() {
const snapshot = order.value?.account_snapshot
if (isRecord(snapshot)) return snapshot
return null
}
function readAssetSummary() {
const summary = readSnapshot()?.asset_summary
if (isRecord(summary)) return summary
return null
}
function readSnapshotResources(): CheckoutResource[] {
const resources = readAssetSummary()?.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)
}
function hydrateResourceUsage() {
const next: Record<string, number> = {}
for (const item of checkoutResources.value) {
next[item.key] = Math.min(Math.max(Number(resourceUsage.value[item.key] || 0), 0), item.quantity)
}
resourceUsage.value = next
}
function readResourceUsage(key: string) {
return Math.max(Number(resourceUsage.value[key] || 0), 0)
}
function isChargedResource(item: CheckoutResource) {
return item.mode !== '赠送'
}
function resourceLineAmount(item: CheckoutResource) {
return roundMoney(readResourceUsage(item.key) * item.unitPrice)
}
function checkoutContentWithSummary() {
const lines = [checkoutForm.value.content.trim()].filter(Boolean)
const usedResources = checkoutResources.value.filter((item) => readResourceUsage(item.key) > 0)
if (usedResources.length) {
lines.push(
`额外消耗品:${usedResources
.map((item) => `${item.label} ${readResourceUsage(item.key)}/${item.quantity}${isChargedResource(item) ? `,扣款¥${resourceLineAmount(item).toFixed(2)}` : ',赠送不扣款'}`)
.join('')}`,
)
}
if (Number(checkoutForm.value.coin_consumed_m || 0) > 0) {
lines.push(`哈夫币消耗:${Number(checkoutForm.value.coin_consumed_m).toFixed(2)}M,预计剩余${remainingHafCoinM.value.toFixed(2)}M`)
}
if (lines.length === 0) {
lines.push('租客发起结账。')
}
return lines.join('\n')
}
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
}
function roundMoney(value: number) {
return Math.round(value * 100) / 100
}
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
}
function hydrateCounterForm() {
if (!order.value?.checkout) return
const checkout = order.value.checkout
@@ -379,12 +512,31 @@ function linesToList(value: string) {
<div v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)" class="order-panel">
<h2>发起结账</h2>
<el-input v-model="checkoutForm.content" type="textarea" :rows="4" placeholder="填写结账说明、租后资产状态或注意事项" />
<div class="checkout-resource-panel panel-action">
<div class="checkout-resource-head">
<strong>额外消耗品</strong>
<span>扣款合计¥{{ resourceChargeAmount.toFixed(2) }}</span>
</div>
<el-empty v-if="checkoutResources.length === 0" description="订单快照中暂无额外消耗品" />
<div v-for="item in checkoutResources" v-else :key="item.key" class="checkout-resource-row">
<div class="checkout-resource-meta">
<strong>{{ item.label }}</strong>
<span>库存 {{ item.quantity }}{{ item.mode }}{{ item.price || '未设置单价' }}</span>
</div>
<el-input-number
v-model="resourceUsage[item.key]"
:min="0"
:max="item.quantity"
:precision="0"
controls-position="right"
/>
<span class="checkout-resource-amount">¥{{ resourceLineAmount(item).toFixed(2) }}</span>
</div>
</div>
<el-form class="form-grid panel-action" label-position="top">
<el-form-item label="消耗扣款(元)">
<el-input-number v-model="checkoutForm.consumable_amount" class="full-control" :min="0" :precision="2" controls-position="right" />
</el-form-item>
<el-form-item label="消耗哈夫币(M">
<el-input-number v-model="checkoutForm.coin_consumed_m" class="full-control" :min="0" :precision="2" controls-position="right" />
<el-input-number v-model="checkoutForm.coin_consumed_m" class="full-control" :min="0" :max="snapshotHafCoinM" :precision="2" controls-position="right" />
<span class="field-hint">订单快照 {{ snapshotHafCoinM.toFixed(2) }}M预计剩余 {{ remainingHafCoinM.toFixed(2) }}M</span>
</el-form-item>
<el-form-item label="其他扣款(元)">
<el-input-number v-model="checkoutForm.other_amount" class="full-control" :min="0" :precision="2" controls-position="right" />
@@ -475,3 +627,51 @@ function linesToList(value: string) {
</div>
</section>
</template>
<style scoped>
.checkout-resource-panel {
display: grid;
gap: 10px;
}
.checkout-resource-head,
.checkout-resource-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 12px;
align-items: center;
}
.checkout-resource-head {
color: #1f2d3d;
}
.checkout-resource-head span,
.checkout-resource-meta span,
.field-hint {
color: #6b7785;
font-size: 13px;
}
.checkout-resource-row {
padding: 10px 0;
border-top: 1px solid #eef1f5;
}
.checkout-resource-meta {
display: grid;
gap: 4px;
}
.checkout-resource-amount {
min-width: 72px;
text-align: right;
color: #ff6a00;
font-weight: 700;
}
.field-hint {
display: block;
margin-top: 6px;
}
</style>