拆分订单详情结账逻辑

This commit is contained in:
yml
2026-06-09 22:07:55 +08:00
parent d68020db4e
commit 3bbe356c3f
9 changed files with 1445 additions and 1402 deletions
@@ -0,0 +1,547 @@
<script setup lang="ts">
import type { CheckoutResource } from '@/features/orders/composables/useOrderSnapshot'
import { money, quantity } from '@/features/orders/composables/useOrderSnapshot'
type CheckoutFormModel = {
content: string
coin_consumed_m: number
otherAmountYuan: number
evidenceText: string
}
const props = withDefaults(
defineProps<{
variant?: 'desktop' | 'mobile'
checkoutForm: CheckoutFormModel
resources: CheckoutResource[]
resourceUsage: Record<string, number>
resourceChargeAmount: number
snapshotHafCoinM: number
remainingHafCoinM: number
returning: boolean
resourceLineAmount: (item: CheckoutResource) => number
}>(),
{ variant: 'desktop' }
)
const emit = defineEmits<{
'update:checkoutForm': [value: CheckoutFormModel]
'update:resourceUsage': [value: Record<string, number>]
submit: []
}>()
function updateCheckoutForm(patch: Partial<CheckoutFormModel>) {
emit('update:checkoutForm', { ...props.checkoutForm, ...patch })
}
function updateResourceUsage(key: string, value: number | string | null) {
emit('update:resourceUsage', {
...props.resourceUsage,
[key]: Number(value) || 0,
})
}
function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event: Event) {
updateCheckoutForm({ [field]: Number((event.target as HTMLInputElement).value) || 0 })
}
</script>
<template>
<section v-if="props.variant === 'mobile'" class="card-section action-card">
<h3 class="section-title">退号发起结账</h3>
<p class="action-hint">使用完毕,请输入在此期间消耗的物资与哈夫币进行结账申请。</p>
<van-field
:model-value="props.checkoutForm.content"
rows="3"
autosize
label="结账备注"
type="textarea"
placeholder="可在此说明物品消耗情况或归还留言"
class="action-field"
@update:model-value="updateCheckoutForm({ content: String($event) })"
/>
<div v-if="props.resources.length" class="resource-usage-panel">
<strong class="resource-panel-title">额外物资消耗登记</strong>
<div v-for="item in props.resources" :key="item.key" class="resource-row">
<div class="resource-meta">
<span class="resource-name">{{ item.label }}</span>
<span class="resource-price-text">
上限 {{ item.quantity }} · {{ item.mode }} · {{ item.price }}
</span>
</div>
<div class="stepper-wrap">
<van-stepper
:model-value="props.resourceUsage[item.key]"
integer
min="0"
:max="item.quantity"
@update:model-value="updateResourceUsage(item.key, $event)"
/>
<span class="resource-line-amount">¥{{ money(props.resourceLineAmount(item)) }}</span>
</div>
</div>
<div class="resource-panel-footer">
<span>物资金额合计:</span>
<strong>¥{{ money(props.resourceChargeAmount) }}</strong>
</div>
</div>
<van-cell-group inset :border="false" class="action-field">
<van-field label="消耗哈夫币" label-width="100px">
<template #input>
<div class="coin-input-wrap">
<input
:value="props.checkoutForm.coin_consumed_m"
type="number"
placeholder="消耗数量"
class="custom-inline-input"
@input="updateNumberField('coin_consumed_m', $event)"
/>
<span class="input-unit">M</span>
</div>
</template>
</van-field>
<div class="coin-hint">
订单快照 {{ quantity(props.snapshotHafCoinM) }}M,预计剩余
{{ quantity(props.remainingHafCoinM) }}M
</div>
<van-field label="其他押金赔付" label-width="100px">
<template #input>
<div class="coin-input-wrap">
<input
:value="props.checkoutForm.otherAmountYuan"
type="number"
placeholder="不含上方额外物资"
class="custom-inline-input"
@input="updateNumberField('otherAmountYuan', $event)"
/>
<span class="input-unit">元</span>
</div>
</template>
</van-field>
<div class="deposit-deduct-hint">仅填写封禁、违规、资产损坏等需要从押金赔付的费用。</div>
</van-cell-group>
<div class="checkout-fee-preview">
<div>
<span>额外消耗品已用</span>
<strong>¥{{ money(props.resourceChargeAmount) }}</strong>
<em>计入实际结算租金</em>
</div>
<div>
<span>其他押金赔付</span>
<strong>¥{{ money(props.checkoutForm.otherAmountYuan) }}</strong>
<em>从押金赔付扣除</em>
</div>
</div>
<van-field
:model-value="props.checkoutForm.evidenceText"
rows="2"
autosize
label="结账证据"
type="textarea"
placeholder="结账截图链接(每行一个)"
class="action-field"
@update:model-value="updateCheckoutForm({ evidenceText: String($event) })"
/>
<van-button
type="primary"
block
round
:loading="props.returning"
loading-text="正在提交结账..."
@click="emit('submit')"
>
提交结账归还
</van-button>
</section>
<div v-else class="form-section checkout-form-section">
<div class="section-header">
<h2>发起结账</h2>
</div>
<div class="form-card">
<el-input
:model-value="props.checkoutForm.content"
type="textarea"
:rows="4"
placeholder="填写结账说明、租后资产状态或注意事项"
@update:model-value="updateCheckoutForm({ content: String($event) })"
/>
<div class="checkout-resource-panel">
<div class="checkout-resource-head">
<strong>额外消耗品</strong>
<span class="amount-highlight">已用金额:¥{{ money(props.resourceChargeAmount) }}</span>
</div>
<el-empty v-if="props.resources.length === 0" description="订单快照中暂无额外消耗品" />
<div v-for="item in props.resources" 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
:model-value="props.resourceUsage[item.key]"
:min="0"
:max="item.quantity"
:precision="0"
controls-position="right"
@update:model-value="updateResourceUsage(item.key, $event)"
/>
<span class="checkout-resource-amount">¥{{ money(props.resourceLineAmount(item)) }}</span>
</div>
</div>
<el-form class="form-grid" label-position="top">
<el-form-item label="消耗哈夫币(M)">
<el-input-number
:model-value="props.checkoutForm.coin_consumed_m"
class="full-control"
:min="0"
:max="props.snapshotHafCoinM"
:precision="2"
controls-position="right"
@update:model-value="updateCheckoutForm({ coin_consumed_m: Number($event) || 0 })"
/>
<span class="field-hint">
订单快照 {{ quantity(props.snapshotHafCoinM) }}M,预计剩余
{{ quantity(props.remainingHafCoinM) }}M
</span>
</el-form-item>
<el-form-item label="其他押金赔付(元)">
<el-input-number
:model-value="props.checkoutForm.otherAmountYuan"
class="full-control"
:min="0"
:precision="0"
controls-position="right"
@update:model-value="updateCheckoutForm({ otherAmountYuan: Number($event) || 0 })"
/>
<span class="field-hint">
不含上方额外消耗品;仅填写封禁、违规、资产损坏等需要从押金赔付的费用。
</span>
</el-form-item>
</el-form>
<div class="checkout-deduct-preview">
<div>
<span>额外消耗品已用</span>
<strong>¥{{ money(props.resourceChargeAmount) }}</strong>
<em>按上方物资数量自动计算,计入实际结算租金</em>
</div>
<div>
<span>其他押金赔付</span>
<strong>¥{{ money(props.checkoutForm.otherAmountYuan) }}</strong>
<em>作为押金赔付扣除,和额外消耗品分开展示</em>
</div>
</div>
<el-input
:model-value="props.checkoutForm.evidenceText"
type="textarea"
:rows="3"
placeholder="结账证据链接,一行一个。可填写截图地址或备注链接"
@update:model-value="updateCheckoutForm({ evidenceText: String($event) })"
/>
<el-button type="primary" size="large" :loading="props.returning" @click="emit('submit')">
发起结账
</el-button>
</div>
</div>
</template>
<style scoped>
.form-section {
margin-bottom: 32px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-header h2 {
font-size: 20px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.form-card {
display: grid;
gap: 16px;
padding: 24px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.checkout-resource-panel {
display: grid;
gap: 12px;
padding: 16px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.checkout-resource-head {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: center;
padding-bottom: 12px;
border-bottom: 2px solid #e5e7eb;
}
.checkout-resource-head strong {
color: #1f2937;
font-size: 15px;
}
.amount-highlight {
color: #ff6a00;
font-weight: 700;
font-size: 14px;
}
.checkout-resource-row {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 12px;
align-items: center;
padding: 12px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
}
.checkout-resource-meta {
display: grid;
gap: 4px;
}
.checkout-resource-meta strong {
color: #1f2937;
font-size: 14px;
}
.checkout-resource-meta span {
color: #6b7785;
font-size: 12px;
}
.checkout-resource-amount {
min-width: 80px;
text-align: right;
color: #ff6a00;
font-weight: 700;
font-size: 16px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.field-hint {
display: block;
margin-top: 6px;
color: #6b7280;
font-size: 12px;
}
.checkout-deduct-preview,
.checkout-fee-preview {
display: grid;
gap: 12px;
}
.checkout-deduct-preview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.checkout-deduct-preview div,
.checkout-fee-preview div {
display: grid;
gap: 6px;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f9fafb;
}
.checkout-deduct-preview span,
.checkout-fee-preview span {
color: #4b5563;
font-size: 13px;
font-weight: 700;
}
.checkout-deduct-preview strong,
.checkout-fee-preview strong {
color: #ff6a00;
font-size: 18px;
font-weight: 800;
}
.checkout-deduct-preview em,
.checkout-fee-preview em {
color: #6b7280;
font-size: 12px;
font-style: normal;
line-height: 1.45;
}
.card-section {
margin: 12px 12px 0;
padding: 14px;
background: #ffffff;
border-radius: 8px;
}
.action-card {
background: #ffffff;
}
.section-title {
font-size: 15px;
font-weight: 800;
color: #182232;
margin: 0 0 12px;
}
.action-hint {
margin: 0 0 12px;
color: #5f6f86;
font-size: 13px;
line-height: 1.55;
}
.action-field {
margin-bottom: 12px;
}
.resource-usage-panel {
margin: 0 16px 12px;
padding: 12px;
border: 1px solid #edf2f7;
border-radius: 8px;
background: #fafafa;
}
.resource-panel-title {
display: block;
font-size: 13px;
font-weight: 700;
margin-bottom: 10px;
color: #2d3748;
}
.resource-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px dashed #e2e8f0;
}
.resource-row:last-child {
border-bottom: none;
}
.resource-meta {
display: flex;
flex-direction: column;
gap: 2px;
}
.resource-name {
font-size: 13px;
color: #2d3748;
font-weight: 600;
}
.resource-price-text {
font-size: 11px;
color: #a0aec0;
}
.stepper-wrap {
display: flex;
align-items: center;
gap: 8px;
}
.resource-line-amount {
min-width: 50px;
text-align: right;
font-size: 13px;
font-weight: 700;
color: #ff5f00;
}
.resource-panel-footer {
display: flex;
justify-content: flex-end;
align-items: baseline;
margin-top: 10px;
font-size: 12px;
color: #4a5568;
}
.resource-panel-footer strong {
font-size: 15px;
color: #ff5f00;
}
.coin-input-wrap {
display: flex;
align-items: center;
width: 100%;
}
.custom-inline-input {
flex: 1;
min-width: 0;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #182232;
}
.input-unit {
color: #8a94a6;
font-size: 13px;
}
.coin-hint,
.deposit-deduct-hint {
padding: 0 16px 10px;
color: #8a94a6;
font-size: 12px;
line-height: 1.5;
}
.checkout-fee-preview {
margin: 0 16px 12px;
}
@media (max-width: 768px) {
.checkout-resource-row {
grid-template-columns: 1fr;
}
.checkout-resource-amount {
text-align: left;
}
.checkout-deduct-preview {
grid-template-columns: 1fr;
}
}
</style>