按订单快照计算结账消耗金额
This commit is contained in:
@@ -48,6 +48,7 @@ const checkoutForm = ref({
|
|||||||
other_amount: 0,
|
other_amount: 0,
|
||||||
evidenceText: '',
|
evidenceText: '',
|
||||||
})
|
})
|
||||||
|
const resourceUsage = ref<Record<string, number>>({})
|
||||||
const counterForm = ref({
|
const counterForm = ref({
|
||||||
consumable_amount: 0,
|
consumable_amount: 0,
|
||||||
coin_consumed_m: 0,
|
coin_consumed_m: 0,
|
||||||
@@ -70,6 +71,26 @@ const canOpenDispute = computed(() => {
|
|||||||
const isCheckoutDisputeStage = computed(() => {
|
const isCheckoutDisputeStage = computed(() => {
|
||||||
return !!order.value && ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.value.status)
|
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)
|
onMounted(loadOrder)
|
||||||
|
|
||||||
@@ -78,6 +99,7 @@ async function loadOrder() {
|
|||||||
try {
|
try {
|
||||||
order.value = await fetchOrder(String(route.params.id))
|
order.value = await fetchOrder(String(route.params.id))
|
||||||
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
|
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
|
||||||
|
hydrateResourceUsage()
|
||||||
hydrateCounterForm()
|
hydrateCounterForm()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -145,9 +167,11 @@ async function handleSubmitCheckout() {
|
|||||||
if (!order.value) return
|
if (!order.value) return
|
||||||
returning.value = true
|
returning.value = true
|
||||||
try {
|
try {
|
||||||
|
const consumableAmount = resourceChargeAmount.value
|
||||||
|
checkoutForm.value.consumable_amount = consumableAmount
|
||||||
await submitCheckout(order.value.id, {
|
await submitCheckout(order.value.id, {
|
||||||
content: checkoutForm.value.content,
|
content: checkoutContentWithSummary(),
|
||||||
consumable_amount: checkoutForm.value.consumable_amount,
|
consumable_amount: consumableAmount,
|
||||||
coin_consumed_m: checkoutForm.value.coin_consumed_m,
|
coin_consumed_m: checkoutForm.value.coin_consumed_m,
|
||||||
other_amount: checkoutForm.value.other_amount,
|
other_amount: checkoutForm.value.other_amount,
|
||||||
evidence_urls: linesToList(checkoutForm.value.evidenceText),
|
evidence_urls: linesToList(checkoutForm.value.evidenceText),
|
||||||
@@ -159,6 +183,7 @@ async function handleSubmitCheckout() {
|
|||||||
other_amount: 0,
|
other_amount: 0,
|
||||||
evidenceText: '',
|
evidenceText: '',
|
||||||
}
|
}
|
||||||
|
resourceUsage.value = {}
|
||||||
ElMessage.success('结账已发起,等待号主确认')
|
ElMessage.success('结账已发起,等待号主确认')
|
||||||
await loadOrder()
|
await loadOrder()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -295,6 +320,114 @@ function orderEstimatedEndAt() {
|
|||||||
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
|
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() {
|
function hydrateCounterForm() {
|
||||||
if (!order.value?.checkout) return
|
if (!order.value?.checkout) return
|
||||||
const checkout = order.value.checkout
|
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">
|
<div v-if="order && isRenter && ['renting', 'overdue'].includes(order.status)" class="order-panel">
|
||||||
<h2>发起结账</h2>
|
<h2>发起结账</h2>
|
||||||
<el-input v-model="checkoutForm.content" type="textarea" :rows="4" placeholder="填写结账说明、租后资产状态或注意事项" />
|
<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 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-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>
|
||||||
<el-form-item label="其他扣款(元)">
|
<el-form-item label="其他扣款(元)">
|
||||||
<el-input-number v-model="checkoutForm.other_amount" class="full-control" :min="0" :precision="2" controls-position="right" />
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</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