按订单快照计算结账消耗金额
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user