feat(order): 结账备注与系统结算信息分开展示,系统结算信息改为逐行明细
- 结账内容写入分隔标记,展示端拆出「结账备注(不参与结算金额)」与「系统结算信息(参与结算金额)」 - 系统结算信息每件消耗品单独一行并给合计,哈夫币/押金各一行;兼容旧数据(同一行用「;」分隔自动拆行并补合计) - 新增 CheckoutSystemDetail 明细组件,接入订单交接记录、结账明细(PC/移动端)与客服订单详情、聊天侧栏 - 结账提交表单与二次确认补充「备注不参与结算金额」提示
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- 创建订单后进入待支付;支付成功后进入待交接并锁定商品。
|
||||
- 号主提交交接信息,租客确认后开始计算租期。
|
||||
- 租客归还后进入结账协商,双方确认或客服仲裁后完成结算。
|
||||
- 结账内容区分「结账备注」与「系统结算信息」:备注仅为双方留言,不参与结算计算;结算金额以系统结算信息(额外消耗品、哈夫币实际消耗、押金赔付扣除)为准。
|
||||
- 订单状态、交接状态和结算状态共同描述流程,禁止仅修改其中一个字段绕过状态机。
|
||||
|
||||
## 支付与退款
|
||||
|
||||
@@ -349,6 +349,30 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 交接记录里区分租客手填备注(不参与结算金额)与系统结算信息(参与结算金额) */
|
||||
.order-side-section .handoff-remark {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e5e9f2;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.order-side-section .handoff-system {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 6px;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.order-side-section .handoff-system-tag {
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-side-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -22,9 +22,11 @@ import {
|
||||
fetchAdminLatestOrderByListing,
|
||||
fetchAdminHandoffRecords,
|
||||
fetchAdminOrder,
|
||||
splitCheckoutContent,
|
||||
type HandoffRecord,
|
||||
type Order,
|
||||
} from '@/features/orders'
|
||||
import CheckoutSystemDetail from '@/features/orders/components/CheckoutSystemDetail.vue'
|
||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import ChatAttachmentImage from '@/components/ChatAttachmentImage.vue'
|
||||
@@ -108,6 +110,8 @@ const activeListingCode = computed(() =>
|
||||
: '-'
|
||||
)
|
||||
const latestHandoffRecord = computed(() => activeHandoffRecords.value[0])
|
||||
// 交接记录内容含「租客手填备注 + 系统结算信息」,拆分展示避免客服把备注当作结算依据
|
||||
const latestHandoffParts = computed(() => splitCheckoutContent(latestHandoffRecord.value?.content))
|
||||
const latestPaymentRecord = computed(() => activePaymentRecords.value[0])
|
||||
const hasOrderContext = computed(() => Boolean(active.value?.order_id || active.value?.listing_id))
|
||||
const orderDetailLink = computed(() =>
|
||||
@@ -894,7 +898,16 @@ function firstQueryValue(value: unknown) {
|
||||
<h3>交接记录</h3>
|
||||
<template v-if="latestHandoffRecord">
|
||||
<strong>{{ formatHandoffRecordType(latestHandoffRecord.type) }}</strong>
|
||||
<p>{{ latestHandoffRecord.content }}</p>
|
||||
<template v-if="latestHandoffParts.systemLines.length">
|
||||
<p v-if="latestHandoffParts.remark" class="handoff-remark">
|
||||
备注(不参与结算金额):{{ latestHandoffParts.remark }}
|
||||
</p>
|
||||
<div class="handoff-system">
|
||||
<span class="handoff-system-tag">系统结算信息(参与结算金额)</span>
|
||||
<CheckoutSystemDetail :lines="latestHandoffParts.systemLines" />
|
||||
</div>
|
||||
</template>
|
||||
<p v-else>{{ latestHandoffRecord.content }}</p>
|
||||
<p>{{ formatDateTime(latestHandoffRecord.created_at) }}</p>
|
||||
</template>
|
||||
<p v-else>暂无交接记录</p>
|
||||
|
||||
@@ -113,6 +113,12 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* 结账备注与系统结算信息为多行文本,保留换行 */
|
||||
.detail-list dd.detail-pre {
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.money-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -192,6 +198,36 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 交接记录里租客手填备注与系统结算信息分块展示 */
|
||||
.record-note {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.record-note.remark {
|
||||
border-color: #e5e9f2;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.record-note.system {
|
||||
border-color: #dbeafe;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.record-note-tag {
|
||||
color: #8f9bba;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.record-note.system .record-note-tag {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.snapshot-detail-list {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,13 @@ import {
|
||||
} from '@/features/orders'
|
||||
import { adminCreateOrderDispute } from '@/features/disputes'
|
||||
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments'
|
||||
import CheckoutSystemDetail from '@/features/orders/components/CheckoutSystemDetail.vue'
|
||||
import {
|
||||
getSnapshotHafCoinM,
|
||||
linesToList,
|
||||
readSnapshot,
|
||||
readSnapshotResources,
|
||||
splitCheckoutContent,
|
||||
} from '@/features/orders/composables/useOrderSnapshot'
|
||||
import { adminPath, isAdminPath } from '@/shared/utils/adminPath'
|
||||
import { centToYuan, formatCentWithSymbol } from '@/shared/utils/money'
|
||||
@@ -143,6 +145,12 @@ function goBackToList() {
|
||||
const refunding = ref(false)
|
||||
|
||||
const snapshotText = computed(() => JSON.stringify(order.value?.account_snapshot || {}, null, 2))
|
||||
// 交接记录内容含「租客手填备注 + 系统结算信息」,拆分展示便于客服区分二者
|
||||
const handoffRecordEntries = computed(() =>
|
||||
handoffRecords.value.map(record => ({ record, parts: splitCheckoutContent(record.content) }))
|
||||
)
|
||||
// 结账说明同样区分手填备注与系统结算信息
|
||||
const checkoutContentParts = computed(() => splitCheckoutContent(order.value?.checkout?.content))
|
||||
const canOperate = computed(
|
||||
() => !!order.value && !['completed', 'cancelled', 'closed'].includes(order.value.status)
|
||||
)
|
||||
@@ -1353,9 +1361,13 @@ function formatRatioNumber(value: number) {
|
||||
<dt>凭证数量</dt>
|
||||
<dd>{{ order.checkout.evidence_urls?.length || 0 }} 张</dd>
|
||||
</div>
|
||||
<div v-if="order.checkout.content" class="wide">
|
||||
<dt>结账说明</dt>
|
||||
<dd>{{ order.checkout.content }}</dd>
|
||||
<div v-if="checkoutContentParts.remark" class="wide">
|
||||
<dt>结账备注(不参与结算金额)</dt>
|
||||
<dd class="detail-pre">{{ checkoutContentParts.remark }}</dd>
|
||||
</div>
|
||||
<div v-if="checkoutContentParts.systemLines.length" class="wide">
|
||||
<dt>系统结算信息(参与结算金额)</dt>
|
||||
<dd><CheckoutSystemDetail :lines="checkoutContentParts.systemLines" /></dd>
|
||||
</div>
|
||||
<div v-if="order.checkout.owner_adjustment_reason" class="wide">
|
||||
<dt>号主修正</dt>
|
||||
@@ -1411,17 +1423,27 @@ function formatRatioNumber(value: number) {
|
||||
</div>
|
||||
<el-empty v-if="handoffRecords.length === 0" description="暂无交接记录" />
|
||||
<div v-else class="record-list">
|
||||
<article v-for="record in handoffRecords" :key="record.id" class="record-item">
|
||||
<article v-for="entry in handoffRecordEntries" :key="entry.record.id" class="record-item">
|
||||
<div class="record-main">
|
||||
<strong>{{ formatHandoffRecordType(record.type) }}</strong>
|
||||
<span>{{ formatDateTime(record.created_at) }}</span>
|
||||
<strong>{{ formatHandoffRecordType(entry.record.type) }}</strong>
|
||||
<span>{{ formatDateTime(entry.record.created_at) }}</span>
|
||||
</div>
|
||||
<p>{{ record.content }}</p>
|
||||
<template v-if="entry.parts.systemLines.length">
|
||||
<div v-if="entry.parts.remark" class="record-note remark">
|
||||
<span class="record-note-tag">备注 · 不参与结算金额</span>
|
||||
<p>{{ entry.parts.remark }}</p>
|
||||
</div>
|
||||
<div class="record-note system">
|
||||
<span class="record-note-tag">系统结算信息 · 参与结算金额</span>
|
||||
<CheckoutSystemDetail :lines="entry.parts.systemLines" />
|
||||
</div>
|
||||
</template>
|
||||
<p v-else>{{ entry.record.content }}</p>
|
||||
<div class="record-meta">
|
||||
<span>发送:ID {{ record.from_user_id }}</span>
|
||||
<span>接收:ID {{ record.to_user_id }}</span>
|
||||
<span>租客确认:{{ formatDateTime(record.confirmed_by_renter_at, '-') }}</span>
|
||||
<span>号主确认:{{ formatDateTime(record.confirmed_by_owner_at, '-') }}</span>
|
||||
<span>发送:ID {{ entry.record.from_user_id }}</span>
|
||||
<span>接收:ID {{ entry.record.to_user_id }}</span>
|
||||
<span>租客确认:{{ formatDateTime(entry.record.confirmed_by_renter_at, '-') }}</span>
|
||||
<span>号主确认:{{ formatDateTime(entry.record.confirmed_by_owner_at, '-') }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { parseCheckoutSystemSummary } from '@/features/orders/composables/useOrderSnapshot'
|
||||
|
||||
const props = defineProps<{
|
||||
/** splitCheckoutContent(content).systemLines,逐行系统结算信息 */
|
||||
lines: string[]
|
||||
}>()
|
||||
|
||||
const summary = computed(() => parseCheckoutSystemSummary(props.lines))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="checkout-system-detail">
|
||||
<div v-if="summary.items.length || summary.totalText" class="detail-group">
|
||||
<span class="group-title">额外消耗品</span>
|
||||
<div v-for="(item, index) in summary.items" :key="`item-${index}`" class="detail-row">
|
||||
<span class="row-label">
|
||||
<span class="row-name">{{ item.name }}</span>
|
||||
<span v-if="item.count" class="row-count">{{ item.count }}</span>
|
||||
</span>
|
||||
<span class="row-value" :class="{ free: item.free }">
|
||||
{{ item.free ? '赠送不扣款' : item.amount }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="summary.totalText" class="detail-row total">
|
||||
<span class="row-label">
|
||||
<span class="row-name">合计</span>
|
||||
</span>
|
||||
<span class="row-value">{{ summary.totalText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<div v-if="summary.coinValue" class="detail-row">
|
||||
<span class="row-label">
|
||||
<span class="row-name">哈夫币实际消耗</span>
|
||||
<span v-if="summary.coinNote" class="row-note">{{ summary.coinNote }}</span>
|
||||
</span>
|
||||
<span class="row-value">{{ summary.coinValue }}</span>
|
||||
</div>
|
||||
<div v-if="summary.depositValue" class="detail-row">
|
||||
<span class="row-label">
|
||||
<span class="row-name">押金赔付扣除</span>
|
||||
<span v-if="summary.depositNote" class="row-note">{{ summary.depositNote }}</span>
|
||||
</span>
|
||||
<span class="row-value warning">{{ summary.depositValue }}</span>
|
||||
</div>
|
||||
<p v-for="(line, index) in summary.extraLines" :key="`extra-${index}`" class="row-extra">
|
||||
{{ line }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.checkout-system-detail {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-group {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
color: #8a94a6;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row-name {
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.row-count,
|
||||
.row-note {
|
||||
color: #8a94a6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.row-value {
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row-value.free {
|
||||
color: #16a34a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row-value.warning {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.detail-row.total {
|
||||
margin-top: 2px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px dashed #cdd9ea;
|
||||
}
|
||||
|
||||
.detail-row.total .row-name {
|
||||
color: #2563eb;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.detail-row.total .row-value {
|
||||
color: #2563eb;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.row-extra {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,12 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { Order } from '@/features/orders/api/orders'
|
||||
import { amountYuan, money } from '@/features/orders/composables/useOrderSnapshot'
|
||||
import CheckoutSystemDetail from '@/features/orders/components/CheckoutSystemDetail.vue'
|
||||
import {
|
||||
amountYuan,
|
||||
money,
|
||||
splitCheckoutContent,
|
||||
} from '@/features/orders/composables/useOrderSnapshot'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -15,6 +20,8 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const checkout = computed(() => props.order.checkout)
|
||||
// 结账内容含手填备注与系统结算信息,拆开展示并标注备注不参与结算
|
||||
const checkoutContent = computed(() => splitCheckoutContent(props.order.checkout?.content))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -97,13 +104,23 @@ const checkout = computed(() => props.order.checkout)
|
||||
:value="`+${Number(props.order.growth_points_awarded || 0)}`"
|
||||
value-class="green-text"
|
||||
/>
|
||||
<van-cell v-if="checkout.content" title="结账备注" :label="checkout.content" />
|
||||
<van-cell
|
||||
v-if="checkout.owner_adjustment_reason"
|
||||
title="号主修正原因"
|
||||
:label="checkout.owner_adjustment_reason"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<div v-if="checkoutContent.remark || checkoutContent.systemLines.length" class="mobile-note">
|
||||
<div v-if="checkoutContent.remark" class="note-block remark">
|
||||
<span class="note-tag">结账备注 · 不参与结算金额</span>
|
||||
<p>{{ checkoutContent.remark }}</p>
|
||||
</div>
|
||||
<div v-if="checkoutContent.systemLines.length" class="note-block system">
|
||||
<span class="note-tag">系统结算信息 · 参与结算金额</span>
|
||||
<CheckoutSystemDetail :lines="checkoutContent.systemLines" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-else-if="checkout" class="info-section">
|
||||
@@ -182,9 +199,13 @@ const checkout = computed(() => props.order.checkout)
|
||||
</span>
|
||||
<strong class="amount">+{{ Number(props.order.growth_points_awarded || 0) }}</strong>
|
||||
</div>
|
||||
<div v-if="checkout.content" class="summary-note">
|
||||
<label>说明:</label>
|
||||
<p>{{ checkout.content }}</p>
|
||||
<div v-if="checkoutContent.remark" class="summary-note">
|
||||
<label>结账备注(仅留言,不参与结算金额):</label>
|
||||
<p>{{ checkoutContent.remark }}</p>
|
||||
</div>
|
||||
<div v-if="checkoutContent.systemLines.length" class="summary-note system">
|
||||
<label>系统结算信息(按上方数据自动生成,参与结算金额):</label>
|
||||
<CheckoutSystemDetail :lines="checkoutContent.systemLines" />
|
||||
</div>
|
||||
<div v-if="checkout.owner_adjustment_reason" class="summary-note warning">
|
||||
<label>修正原因:</label>
|
||||
@@ -288,6 +309,10 @@ const checkout = computed(() => props.order.checkout)
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
.summary-note.system {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.summary-note label {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
@@ -311,6 +336,45 @@ const checkout = computed(() => props.order.checkout)
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mobile-note {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.note-block {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #eef1f5;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.note-block.system {
|
||||
border-color: #dbeafe;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.note-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
.note-block.system .note-tag {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.note-block p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { HandoffRecord } from '@/features/orders/api/orders'
|
||||
import { formatHandoffRecordType } from '@/features/orders/composables/useOrderSnapshot'
|
||||
import CheckoutSystemDetail from '@/features/orders/components/CheckoutSystemDetail.vue'
|
||||
import {
|
||||
formatHandoffRecordType,
|
||||
splitCheckoutContent,
|
||||
} from '@/features/orders/composables/useOrderSnapshot'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -11,6 +17,14 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{ variant: 'desktop' }
|
||||
)
|
||||
|
||||
/**
|
||||
* 结账类记录内容包含「手填备注 + 系统结算信息」,
|
||||
* 拆开展示并标注备注不参与结算,避免号主把备注当成结算依据。
|
||||
*/
|
||||
const recordEntries = computed(() =>
|
||||
props.records.map(record => ({ record, parts: splitCheckoutContent(record.content) }))
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -18,12 +32,22 @@ const props = withDefaults(
|
||||
<h3 class="section-title">交接日志 · 商品编号 {{ props.listingCode }}</h3>
|
||||
<van-empty v-if="props.records.length === 0" image="search" description="暂无交接日志记录" />
|
||||
<div v-else class="log-timeline">
|
||||
<div v-for="record in props.records" :key="record.id" class="log-item">
|
||||
<div v-for="entry in recordEntries" :key="entry.record.id" class="log-item">
|
||||
<div class="log-dot"></div>
|
||||
<div class="log-content-wrap">
|
||||
<strong class="log-type">{{ formatHandoffRecordType(record.type) }}</strong>
|
||||
<p class="log-desc">{{ record.content }}</p>
|
||||
<span class="log-time">{{ formatDateTime(record.created_at) }}</span>
|
||||
<strong class="log-type">{{ formatHandoffRecordType(entry.record.type) }}</strong>
|
||||
<template v-if="entry.parts.systemLines.length">
|
||||
<div v-if="entry.parts.remark" class="body-block remark-block">
|
||||
<span class="block-tag">备注 · 不参与结算金额</span>
|
||||
<p class="block-text">{{ entry.parts.remark }}</p>
|
||||
</div>
|
||||
<div class="body-block system-block">
|
||||
<span class="block-tag">系统结算信息 · 参与结算金额</span>
|
||||
<CheckoutSystemDetail :lines="entry.parts.systemLines" />
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="log-desc">{{ entry.record.content }}</p>
|
||||
<span class="log-time">{{ formatDateTime(entry.record.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,11 +61,23 @@ const props = withDefaults(
|
||||
<div class="timeline-card">
|
||||
<div v-if="props.records.length === 0" class="empty-log">暂无交接记录</div>
|
||||
<div v-else class="compact-timeline">
|
||||
<div v-for="record in props.records" :key="record.id" class="timeline-row">
|
||||
<div v-for="entry in recordEntries" :key="entry.record.id" class="timeline-row">
|
||||
<span class="timeline-dot"></span>
|
||||
<span class="timeline-time">{{ formatDateTime(record.created_at) }}</span>
|
||||
<strong class="timeline-title">{{ formatHandoffRecordType(record.type) }}</strong>
|
||||
<p class="timeline-body">{{ record.content }}</p>
|
||||
<span class="timeline-time">{{ formatDateTime(entry.record.created_at) }}</span>
|
||||
<strong class="timeline-title">{{ formatHandoffRecordType(entry.record.type) }}</strong>
|
||||
<div class="timeline-body">
|
||||
<template v-if="entry.parts.systemLines.length">
|
||||
<div v-if="entry.parts.remark" class="body-block remark-block">
|
||||
<span class="block-tag">备注 · 不参与结算金额</span>
|
||||
<p class="block-text">{{ entry.parts.remark }}</p>
|
||||
</div>
|
||||
<div class="body-block system-block">
|
||||
<span class="block-tag">系统结算信息 · 参与结算金额</span>
|
||||
<CheckoutSystemDetail :lines="entry.parts.systemLines" />
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="block-text">{{ entry.record.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,10 +158,47 @@ const props = withDefaults(
|
||||
}
|
||||
|
||||
.timeline-body {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.body-block {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #eef1f5;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.remark-block {
|
||||
border-color: #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.system-block {
|
||||
border-color: #dbeafe;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.block-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
.system-block .block-tag {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.block-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -57,10 +57,13 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
||||
autosize
|
||||
label="结账备注"
|
||||
type="textarea"
|
||||
placeholder="可在此说明物品消耗情况或归还留言"
|
||||
placeholder="仅作留言沟通,不参与结算金额计算"
|
||||
class="action-field"
|
||||
@update:model-value="updateCheckoutForm({ content: String($event) })"
|
||||
/>
|
||||
<p class="remark-hint">
|
||||
「结账备注」仅作留言沟通,不参与结算:租金、押金赔付和最终结算金额以「额外消耗品」「哈夫币实际消耗」「押金赔付扣除」为准。
|
||||
</p>
|
||||
|
||||
<div v-if="props.resources.length" class="resource-usage-panel">
|
||||
<strong class="resource-panel-title">额外物资消耗登记</strong>
|
||||
@@ -179,13 +182,22 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
||||
<span>使用完毕后填写租后状态和消耗明细</span>
|
||||
</div>
|
||||
<div class="form-card checkout-task-card">
|
||||
<el-input
|
||||
:model-value="props.checkoutForm.content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="填写结账说明、租后资产状态或注意事项"
|
||||
@update:model-value="updateCheckoutForm({ content: String($event) })"
|
||||
/>
|
||||
<div class="checkout-remark-field">
|
||||
<div class="checkout-remark-head">
|
||||
<strong>结账备注</strong>
|
||||
<span class="remark-tag">仅留言 · 不参与结算金额</span>
|
||||
</div>
|
||||
<el-input
|
||||
:model-value="props.checkoutForm.content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="填写租后资产状态、归还留言等;备注不会计入结算金额"
|
||||
@update:model-value="updateCheckoutForm({ content: String($event) })"
|
||||
/>
|
||||
<p class="remark-hint">
|
||||
备注仅供双方沟通,不参与租金、押金赔付和最终结算金额计算;结算金额以下方「额外消耗品」「哈夫币实际消耗」「押金赔付扣除」为准。
|
||||
</p>
|
||||
</div>
|
||||
<div class="checkout-resource-panel">
|
||||
<div class="checkout-resource-head">
|
||||
<strong>额外消耗品</strong>
|
||||
@@ -332,6 +344,38 @@ function updateNumberField(field: 'coin_consumed_m' | 'otherAmountYuan', event:
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eff6ff 100%);
|
||||
}
|
||||
|
||||
.checkout-remark-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.checkout-remark-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkout-remark-head strong {
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.remark-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.remark-hint {
|
||||
margin: 0;
|
||||
color: #8a94a6;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.checkout-resource-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@@ -382,6 +382,7 @@ export function useOrderActions(options: UseOrderActionsOptions) {
|
||||
title: '确认提交结账',
|
||||
message:
|
||||
'结账数据会影响租金、押金赔付和双方结算金额。请确认哈夫币消耗、额外消耗品、押金赔付和证据链接已认真核对,提交后将发送给号主确认。' +
|
||||
'\n\n「结账备注」仅作留言沟通,不参与结算金额计算。' +
|
||||
depositWarning,
|
||||
})
|
||||
if (!ok) return
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { Order } from '../api/orders'
|
||||
import { orderListRentalTerms, orderRentalTerms } from './useOrderSnapshot'
|
||||
import {
|
||||
buildCheckoutContentWithSummary,
|
||||
CHECKOUT_SYSTEM_SUMMARY_MARKER,
|
||||
orderListRentalTerms,
|
||||
orderRentalTerms,
|
||||
parseCheckoutSystemSummary,
|
||||
readCheckoutSystemSummary,
|
||||
splitCheckoutContent,
|
||||
type CheckoutResource,
|
||||
} from './useOrderSnapshot'
|
||||
|
||||
describe('orderRentalTerms', () => {
|
||||
it('estimates rental days from the account snapshot', () => {
|
||||
@@ -30,3 +39,227 @@ describe('orderListRentalTerms', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const ammoResource: CheckoutResource = {
|
||||
key: 'ammo6',
|
||||
label: '6级弹',
|
||||
price: '1元/3',
|
||||
mode: '收费',
|
||||
quantity: 10,
|
||||
unitPrice: 0.33,
|
||||
}
|
||||
|
||||
const awmResource: CheckoutResource = {
|
||||
key: 'awm',
|
||||
label: 'AWM子弹',
|
||||
price: '0.6元/1',
|
||||
mode: '收费',
|
||||
quantity: 10,
|
||||
unitPrice: 0.6,
|
||||
}
|
||||
|
||||
const giftResource: CheckoutResource = {
|
||||
key: 'gift',
|
||||
label: '8号弹',
|
||||
price: '1元/1',
|
||||
mode: '赠送',
|
||||
quantity: 5,
|
||||
unitPrice: 1,
|
||||
}
|
||||
|
||||
describe('buildCheckoutContentWithSummary', () => {
|
||||
it('分隔手填备注与系统结算信息,消耗品逐行输出并给出合计', () => {
|
||||
const content = buildCheckoutContentWithSummary({
|
||||
content: '16个6甲2个6头59发子弹你看对',
|
||||
coinConsumedM: 150,
|
||||
depositDeductAmountYuan: 1.8,
|
||||
checkoutResources: [ammoResource, awmResource, giftResource],
|
||||
resourceUsage: { ammo6: 3, awm: 2, gift: 1 },
|
||||
remainingHafCoinM: 0,
|
||||
})
|
||||
|
||||
expect(content).toBe(
|
||||
[
|
||||
'16个6甲2个6头59发子弹你看对',
|
||||
CHECKOUT_SYSTEM_SUMMARY_MARKER,
|
||||
'额外消耗品:6级弹 3/10,金额¥1.0',
|
||||
'额外消耗品:AWM子弹 2/10,金额¥1.2',
|
||||
'额外消耗品:8号弹 1/5,赠送不扣款',
|
||||
'额外消耗品合计:¥2.2',
|
||||
'哈夫币实际消耗:150M,预计剩余0M',
|
||||
'押金赔付扣除:¥1.80(将直接从预收押金中扣除)',
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
it('无结算信息时只保留备注,不写分隔标记', () => {
|
||||
const content = buildCheckoutContentWithSummary({
|
||||
content: ' 归还账号 ',
|
||||
coinConsumedM: 0,
|
||||
checkoutResources: [],
|
||||
resourceUsage: {},
|
||||
remainingHafCoinM: 0,
|
||||
})
|
||||
|
||||
expect(content).toBe('归还账号')
|
||||
expect(content).not.toContain(CHECKOUT_SYSTEM_SUMMARY_MARKER)
|
||||
})
|
||||
|
||||
it('备注与结算信息都为空时给出兜底文案', () => {
|
||||
expect(
|
||||
buildCheckoutContentWithSummary({
|
||||
content: '',
|
||||
coinConsumedM: 0,
|
||||
checkoutResources: [],
|
||||
resourceUsage: {},
|
||||
remainingHafCoinM: 0,
|
||||
})
|
||||
).toBe('租客发起结账。')
|
||||
})
|
||||
|
||||
it('未填备注时仅输出系统结算信息', () => {
|
||||
const content = buildCheckoutContentWithSummary({
|
||||
content: '',
|
||||
coinConsumedM: 20,
|
||||
checkoutResources: [],
|
||||
resourceUsage: {},
|
||||
remainingHafCoinM: 5,
|
||||
})
|
||||
|
||||
expect(splitCheckoutContent(content)).toEqual({
|
||||
remark: '',
|
||||
systemLines: ['哈夫币实际消耗:20M,预计剩余5M'],
|
||||
systemText: '哈夫币实际消耗:20M,预计剩余5M',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('splitCheckoutContent', () => {
|
||||
it('按分隔标记拆分备注与系统结算信息', () => {
|
||||
const parts = splitCheckoutContent(
|
||||
[
|
||||
'第一行备注',
|
||||
'第二行备注',
|
||||
CHECKOUT_SYSTEM_SUMMARY_MARKER,
|
||||
'哈夫币实际消耗:150M,预计剩余0M',
|
||||
].join('\n')
|
||||
)
|
||||
|
||||
expect(parts.remark).toBe('第一行备注\n第二行备注')
|
||||
expect(parts.systemLines).toEqual(['哈夫币实际消耗:150M,预计剩余0M'])
|
||||
expect(parts.systemText).toBe('哈夫币实际消耗:150M,预计剩余0M')
|
||||
})
|
||||
|
||||
it('兼容无标记的历史数据,按前缀识别系统行', () => {
|
||||
const parts = splitCheckoutContent(
|
||||
[
|
||||
'16个6甲2个6头59发子弹你看对',
|
||||
'额外消耗品:6级弹 3/10,金额¥0.3',
|
||||
'哈夫币实际消耗:150M,预计剩余0M',
|
||||
].join('\n')
|
||||
)
|
||||
|
||||
expect(parts.remark).toBe('16个6甲2个6头59发子弹你看对')
|
||||
expect(parts.systemLines).toEqual([
|
||||
'额外消耗品:6级弹 3/10,金额¥0.3',
|
||||
'哈夫币实际消耗:150M,预计剩余0M',
|
||||
])
|
||||
})
|
||||
|
||||
it('普通交接说明整体视为备注', () => {
|
||||
expect(splitCheckoutContent('账号已交接,密码见群聊')).toEqual({
|
||||
remark: '账号已交接,密码见群聊',
|
||||
systemLines: [],
|
||||
systemText: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('空内容返回空结构', () => {
|
||||
expect(splitCheckoutContent('')).toEqual({ remark: '', systemLines: [], systemText: '' })
|
||||
expect(splitCheckoutContent(null)).toEqual({ remark: '', systemLines: [], systemText: '' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCheckoutSystemSummary', () => {
|
||||
it('把逐行系统结算信息解析成明细行', () => {
|
||||
const summary = parseCheckoutSystemSummary([
|
||||
'额外消耗品:6级弹 3/10,金额¥1.0',
|
||||
'额外消耗品:AWM子弹 2/10,金额¥1.2',
|
||||
'额外消耗品:8号弹 1/5,赠送不扣款',
|
||||
'额外消耗品合计:¥2.2',
|
||||
'哈夫币实际消耗:150M,预计剩余0M',
|
||||
'押金赔付扣除:¥1.80(将直接从预收押金中扣除)',
|
||||
])
|
||||
|
||||
expect(summary.items).toEqual([
|
||||
{ name: '6级弹', count: '3/10', amount: '¥1.0', free: false },
|
||||
{ name: 'AWM子弹', count: '2/10', amount: '¥1.2', free: false },
|
||||
{ name: '8号弹', count: '1/5', amount: '', free: true },
|
||||
])
|
||||
expect(summary.totalText).toBe('¥2.2')
|
||||
expect(summary.coinValue).toBe('150M')
|
||||
expect(summary.coinNote).toBe('预计剩余0M')
|
||||
expect(summary.depositValue).toBe('¥1.80')
|
||||
expect(summary.depositNote).toBe('将直接从预收押金中扣除')
|
||||
expect(summary.empty).toBe(false)
|
||||
})
|
||||
|
||||
it('兼容旧数据:同一行的多件消耗品自动拆行并补出合计', () => {
|
||||
const summary = parseCheckoutSystemSummary([
|
||||
'额外消耗品:6级弹 3/10,金额¥0.3;AWM子弹 2/10,金额¥1.2;巴雷特子弹 1/10,金额¥0.3',
|
||||
'哈夫币实际消耗:150M,预计剩余0M',
|
||||
])
|
||||
|
||||
expect(summary.items.map(item => item.name)).toEqual(['6级弹', 'AWM子弹', '巴雷特子弹'])
|
||||
expect(summary.items.map(item => item.amount)).toEqual(['¥0.3', '¥1.2', '¥0.3'])
|
||||
expect(summary.totalText).toBe('¥1.8')
|
||||
})
|
||||
|
||||
it('保留打超说明与未识别的其它系统行', () => {
|
||||
const summary = parseCheckoutSystemSummary([
|
||||
'哈夫币实际消耗:200M,打超50M(超额按比例从押金结算)',
|
||||
'其它系统说明',
|
||||
])
|
||||
|
||||
expect(summary.coinValue).toBe('200M')
|
||||
expect(summary.coinNote).toBe('打超50M(超额按比例从押金结算)')
|
||||
expect(summary.extraLines).toEqual(['其它系统说明'])
|
||||
})
|
||||
|
||||
it('空输入返回空结构', () => {
|
||||
expect(parseCheckoutSystemSummary(null).empty).toBe(true)
|
||||
expect(parseCheckoutSystemSummary([]).empty).toBe(true)
|
||||
})
|
||||
|
||||
it('生成的结账内容可直接读回明细', () => {
|
||||
const content = buildCheckoutContentWithSummary({
|
||||
content: '归还留言',
|
||||
coinConsumedM: 150,
|
||||
checkoutResources: [ammoResource, awmResource],
|
||||
resourceUsage: { ammo6: 3, awm: 2 },
|
||||
remainingHafCoinM: 0,
|
||||
})
|
||||
|
||||
const summary = readCheckoutSystemSummary(content)
|
||||
expect(summary.items.map(item => item.name)).toEqual(['6级弹', 'AWM子弹'])
|
||||
expect(summary.totalText).toBe('¥2.2')
|
||||
expect(summary.coinValue).toBe('150M')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildCheckoutContentWithSummary 消耗品明细', () => {
|
||||
it('全部为赠送品时不输出合计行', () => {
|
||||
const content = buildCheckoutContentWithSummary({
|
||||
content: '仅赠送品',
|
||||
coinConsumedM: 0,
|
||||
checkoutResources: [giftResource],
|
||||
resourceUsage: { gift: 2 },
|
||||
remainingHafCoinM: 0,
|
||||
})
|
||||
|
||||
expect(content).toBe(
|
||||
['仅赠送品', CHECKOUT_SYSTEM_SUMMARY_MARKER, '额外消耗品:8号弹 2/5,赠送不扣款'].join('\n')
|
||||
)
|
||||
expect(readCheckoutSystemSummary(content).totalText).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,6 +26,192 @@ export interface CheckoutResource {
|
||||
unitPrice: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 结账内容分隔标记:写在「租客手填备注」与「系统结算信息」之间。
|
||||
* 提交时写入,展示端据此拆分;历史数据没有标记,按行前缀兜底识别。
|
||||
*/
|
||||
export const CHECKOUT_SYSTEM_SUMMARY_MARKER = '【系统结算信息·参与结算】'
|
||||
|
||||
/** 系统结算信息的行前缀(单件消耗品、合计、哈夫币、押金)。 */
|
||||
const CHECKOUT_CONSUMABLE_PREFIX = '额外消耗品:'
|
||||
const CHECKOUT_CONSUMABLE_TOTAL_PREFIX = '额外消耗品合计:'
|
||||
const CHECKOUT_COIN_PREFIX = '哈夫币实际消耗:'
|
||||
const CHECKOUT_DEPOSIT_PREFIX = '押金赔付扣除:'
|
||||
|
||||
/** 用于识别未带分隔标记的历史数据。 */
|
||||
const CHECKOUT_SYSTEM_LINE_PREFIXES = [
|
||||
CHECKOUT_CONSUMABLE_PREFIX,
|
||||
CHECKOUT_CONSUMABLE_TOTAL_PREFIX,
|
||||
CHECKOUT_COIN_PREFIX,
|
||||
CHECKOUT_DEPOSIT_PREFIX,
|
||||
]
|
||||
|
||||
export interface CheckoutContentParts {
|
||||
/** 租客手填备注:仅作双方沟通留言,不参与结算计算。 */
|
||||
remark: string
|
||||
/** 系统按实际消耗生成的结算信息行。 */
|
||||
systemLines: string[]
|
||||
/** 系统结算信息整体文本,无则为空串。 */
|
||||
systemText: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分结账内容为「手填备注」与「系统结算信息」。
|
||||
* 备注不参与租金、押金赔付和最终结算金额计算,仅用于沟通留痕。
|
||||
*/
|
||||
export function splitCheckoutContent(content: string | null | undefined): CheckoutContentParts {
|
||||
const lines = linesToList(String(content || ''))
|
||||
const markerIndex = lines.indexOf(CHECKOUT_SYSTEM_SUMMARY_MARKER)
|
||||
if (markerIndex >= 0) {
|
||||
const systemLines = lines.slice(markerIndex + 1)
|
||||
return {
|
||||
remark: lines.slice(0, markerIndex).join('\n'),
|
||||
systemLines,
|
||||
systemText: systemLines.join('\n'),
|
||||
}
|
||||
}
|
||||
// 兼容旧数据:系统行按固定前缀识别,其余行视为手填备注
|
||||
const remarkLines: string[] = []
|
||||
const systemLines: string[] = []
|
||||
for (const line of lines) {
|
||||
const isSystemLine = CHECKOUT_SYSTEM_LINE_PREFIXES.some(prefix => line.startsWith(prefix))
|
||||
if (isSystemLine) systemLines.push(line)
|
||||
else remarkLines.push(line)
|
||||
}
|
||||
return { remark: remarkLines.join('\n'), systemLines, systemText: systemLines.join('\n') }
|
||||
}
|
||||
|
||||
/** 单件额外消耗品明细行。 */
|
||||
export interface CheckoutSystemItem {
|
||||
/** 消耗品名称,如 6级弹。 */
|
||||
name: string
|
||||
/** 用量与库存上限,如 3/10;无法解析时为空串。 */
|
||||
count: string
|
||||
/** 金额文本,如 ¥0.3;赠送品为空串。 */
|
||||
amount: string
|
||||
/** 是否赠送(不扣款)。 */
|
||||
free: boolean
|
||||
}
|
||||
|
||||
/** 系统结算信息结构化结果,用于逐行明细展示。 */
|
||||
export interface CheckoutSystemSummary {
|
||||
/** 额外消耗品明细 */
|
||||
items: CheckoutSystemItem[]
|
||||
/** 额外消耗品合计文本,如 ¥1.8 */
|
||||
totalText: string
|
||||
/** 哈夫币实际消耗,如 150M */
|
||||
coinValue: string
|
||||
/** 哈夫币剩余 / 打超说明 */
|
||||
coinNote: string
|
||||
/** 押金赔付扣除金额,如 ¥1.80 */
|
||||
depositValue: string
|
||||
/** 押金扣除说明 */
|
||||
depositNote: string
|
||||
/** 未识别出的其它系统行,原样展示 */
|
||||
extraLines: string[]
|
||||
/** 是否没有任何可结构化的系统信息 */
|
||||
empty: boolean
|
||||
}
|
||||
|
||||
/** 解析单件消耗品文本:`6级弹 3/10,金额¥0.3` / `8号弹 2/5,赠送不扣款`。 */
|
||||
function parseCheckoutConsumableSegment(segment: string): CheckoutSystemItem | null {
|
||||
const text = String(segment || '').trim()
|
||||
if (!text) return null
|
||||
const matched = text.match(/^(.*?)\s*([\d.]+\s*\/\s*[\d.]+)\s*(.*)$/)
|
||||
const count = (matched?.[2] || '').replace(/\s+/g, '')
|
||||
if (!count) return { name: text, count: '', amount: '', free: false }
|
||||
const tail = matched?.[3] || ''
|
||||
const amountMatch = tail.match(/金额¥([\d.]+)/)
|
||||
const amountText = amountMatch?.[1] || ''
|
||||
return {
|
||||
name: (matched?.[1] || text).trim(),
|
||||
count,
|
||||
amount: amountText ? `¥${amountText}` : '',
|
||||
free: !amountText && /赠送/.test(tail),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把系统结算信息行解析成可逐行渲染的明细。
|
||||
* 兼容历史数据:多件消耗品挤在同一行(用「;」分隔)时自动拆行,
|
||||
* 且没有合计行时按各件金额累加补出合计。
|
||||
*/
|
||||
export function parseCheckoutSystemSummary(lines: unknown): CheckoutSystemSummary {
|
||||
const items: CheckoutSystemItem[] = []
|
||||
const extraLines: string[] = []
|
||||
let totalText = ''
|
||||
let coinValue = ''
|
||||
let coinNote = ''
|
||||
let depositValue = ''
|
||||
let depositNote = ''
|
||||
let chargedTotal = 0
|
||||
let chargedAmountKnown = true
|
||||
|
||||
for (const rawLine of Array.isArray(lines) ? lines : []) {
|
||||
const line = String(rawLine || '').trim()
|
||||
if (!line) continue
|
||||
if (line.startsWith(CHECKOUT_CONSUMABLE_TOTAL_PREFIX)) {
|
||||
const value = line.slice(CHECKOUT_CONSUMABLE_TOTAL_PREFIX.length).replace(/^¥/, '').trim()
|
||||
totalText = `¥${value}`
|
||||
continue
|
||||
}
|
||||
if (line.startsWith(CHECKOUT_CONSUMABLE_PREFIX)) {
|
||||
for (const segment of line.slice(CHECKOUT_CONSUMABLE_PREFIX.length).split(';')) {
|
||||
const item = parseCheckoutConsumableSegment(segment)
|
||||
if (!item) continue
|
||||
items.push(item)
|
||||
if (item.free) continue
|
||||
const amount = Number(item.amount.replace(/^¥/, ''))
|
||||
if (item.amount && Number.isFinite(amount)) chargedTotal = roundMoney(chargedTotal + amount)
|
||||
else chargedAmountKnown = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (line.startsWith(CHECKOUT_COIN_PREFIX)) {
|
||||
const [value = '', ...rest] = line.slice(CHECKOUT_COIN_PREFIX.length).trim().split(',')
|
||||
coinValue = value.trim()
|
||||
coinNote = rest.join(',').trim()
|
||||
continue
|
||||
}
|
||||
if (line.startsWith(CHECKOUT_DEPOSIT_PREFIX)) {
|
||||
const body = line.slice(CHECKOUT_DEPOSIT_PREFIX.length).trim()
|
||||
const matched = body.match(/^(.*?)[((](.*?)[))]?$/)
|
||||
if (matched) {
|
||||
depositValue = (matched[1] || '').trim()
|
||||
depositNote = (matched[2] || '').trim()
|
||||
} else {
|
||||
depositValue = body
|
||||
}
|
||||
continue
|
||||
}
|
||||
extraLines.push(line)
|
||||
}
|
||||
|
||||
const hasChargedItem = items.some(item => !item.free)
|
||||
if (!totalText && hasChargedItem && chargedAmountKnown) {
|
||||
totalText = `¥${money(chargedTotal)}`
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
totalText,
|
||||
coinValue,
|
||||
coinNote,
|
||||
depositValue,
|
||||
depositNote,
|
||||
extraLines,
|
||||
empty:
|
||||
items.length === 0 && !totalText && !coinValue && !depositValue && extraLines.length === 0,
|
||||
}
|
||||
}
|
||||
|
||||
/** 直接从结账内容读取系统结算信息明细。 */
|
||||
export function readCheckoutSystemSummary(
|
||||
content: string | null | undefined
|
||||
): CheckoutSystemSummary {
|
||||
return parseCheckoutSystemSummary(splitCheckoutContent(content).systemLines)
|
||||
}
|
||||
|
||||
export function useOrderCheckoutSnapshot(options: {
|
||||
order: Ref<Order | null>
|
||||
checkoutForm: Ref<CheckoutFormSnapshot>
|
||||
@@ -151,43 +337,54 @@ export function buildCheckoutContentWithSummary(options: {
|
||||
resourceUsage: Record<string, number>
|
||||
remainingHafCoinM: number
|
||||
}) {
|
||||
const lines = [options.content.trim()].filter(Boolean)
|
||||
// 手填备注与系统结算信息分开生成:备注仅为留言,结算信息由平台按实际消耗自动计算
|
||||
const remark = options.content.trim()
|
||||
const systemLines: string[] = []
|
||||
const usedResources = options.checkoutResources.filter(
|
||||
item => readResourceUsageAmount(options.resourceUsage, item.key) > 0
|
||||
)
|
||||
if (usedResources.length) {
|
||||
lines.push(
|
||||
`额外消耗品:${usedResources
|
||||
.map(item => {
|
||||
const used = readResourceUsageAmount(options.resourceUsage, item.key)
|
||||
const amount = roundMoney(used * item.unitPrice)
|
||||
return `${item.label} ${used}/${item.quantity}${
|
||||
isChargedResource(item) ? `,金额¥${money(amount)}` : ',赠送不扣款'
|
||||
}`
|
||||
})
|
||||
.join(';')}`
|
||||
// 每件消耗品单独一行,便于展示端渲染成明细行并补出合计
|
||||
let consumableTotalYuan = 0
|
||||
let hasChargedResource = false
|
||||
for (const item of usedResources) {
|
||||
const used = readResourceUsageAmount(options.resourceUsage, item.key)
|
||||
const charged = isChargedResource(item)
|
||||
const amount = roundMoney(used * item.unitPrice)
|
||||
if (charged) {
|
||||
hasChargedResource = true
|
||||
consumableTotalYuan = roundMoney(consumableTotalYuan + amount)
|
||||
}
|
||||
systemLines.push(
|
||||
`${CHECKOUT_CONSUMABLE_PREFIX}${item.label} ${used}/${item.quantity}${
|
||||
charged ? `,金额¥${money(amount)}` : ',赠送不扣款'
|
||||
}`
|
||||
)
|
||||
}
|
||||
if (hasChargedResource) {
|
||||
systemLines.push(`${CHECKOUT_CONSUMABLE_TOTAL_PREFIX}¥${money(consumableTotalYuan)}`)
|
||||
}
|
||||
if (Number(options.coinConsumedM || 0) > 0) {
|
||||
const remain = Number(options.remainingHafCoinM || 0)
|
||||
if (remain >= 0) {
|
||||
lines.push(
|
||||
`哈夫币实际消耗:${quantity(options.coinConsumedM)}M,预计剩余${quantity(remain)}M`
|
||||
systemLines.push(
|
||||
`${CHECKOUT_COIN_PREFIX}${quantity(options.coinConsumedM)}M,预计剩余${quantity(remain)}M`
|
||||
)
|
||||
} else {
|
||||
lines.push(
|
||||
`哈夫币实际消耗:${quantity(options.coinConsumedM)}M,打超${quantity(-remain)}M(超额按比例从押金结算)`
|
||||
systemLines.push(
|
||||
`${CHECKOUT_COIN_PREFIX}${quantity(options.coinConsumedM)}M,打超${quantity(-remain)}M(超额按比例从押金结算)`
|
||||
)
|
||||
}
|
||||
}
|
||||
const depositDeductAmountYuan = Number(options.depositDeductAmountYuan || 0)
|
||||
if (depositDeductAmountYuan > 0) {
|
||||
lines.push(`押金赔付扣除:¥${depositDeductAmountYuan.toFixed(2)}(将直接从预收押金中扣除)`)
|
||||
systemLines.push(
|
||||
`${CHECKOUT_DEPOSIT_PREFIX}¥${depositDeductAmountYuan.toFixed(2)}(将直接从预收押金中扣除)`
|
||||
)
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
lines.push('租客发起结账。')
|
||||
if (systemLines.length === 0) {
|
||||
return remark || '租客发起结账。'
|
||||
}
|
||||
return lines.join('\n')
|
||||
return [remark, CHECKOUT_SYSTEM_SUMMARY_MARKER, ...systemLines].filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
export function hydrateCounterFormFromCheckout(checkout: Checkout): CounterFormSnapshot {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Orders 模块统一导出
|
||||
export * from './api/orders'
|
||||
export * from './composables/useOrderSnapshot'
|
||||
export { default as CheckoutSystemDetail } from './components/CheckoutSystemDetail.vue'
|
||||
export { default as OrderAccountSnapshot } from './components/OrderAccountSnapshot.vue'
|
||||
export { default as OrderCheckoutSummary } from './components/OrderCheckoutSummary.vue'
|
||||
export { default as OrderHandoffTimeline } from './components/OrderHandoffTimeline.vue'
|
||||
|
||||
Reference in New Issue
Block a user