Files
hfb_sys/frontend/src/features/orders/components/OrderResourceUsageEditor.vue
T
2026-06-09 22:07:55 +08:00

548 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>