feat(listings): 订单和账号详情展示可租时长与每日消耗
租客在订单列表和详情标题能看到每个号的租期、消耗。账号详情把这两项拆成独立格子。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -89,6 +89,8 @@ export interface UserOrderListItem {
|
||||
title: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rental_days?: number
|
||||
daily_loss_m?: number
|
||||
display_amount_cent: number
|
||||
deposit_amount_cent: number
|
||||
deposit_waived_amount_cent: number
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { Order } from '../api/orders'
|
||||
import { orderListRentalTerms, orderRentalTerms } from './useOrderSnapshot'
|
||||
|
||||
describe('orderRentalTerms', () => {
|
||||
it('estimates rental days from the account snapshot', () => {
|
||||
const order = {
|
||||
account_snapshot: {
|
||||
haf_coin_amount: 100_000_000,
|
||||
asset_summary: { daily_loss_m: 10 },
|
||||
},
|
||||
} as Order
|
||||
|
||||
expect(orderRentalTerms(order)).toEqual({ rentalDays: 10, dailyLossText: '10M/天' })
|
||||
})
|
||||
|
||||
it('hides terms when the snapshot has no daily loss', () => {
|
||||
expect(
|
||||
orderRentalTerms({ account_snapshot: { haf_coin_amount: 100_000_000 } } as Order)
|
||||
).toEqual({ rentalDays: 0, dailyLossText: '' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('orderListRentalTerms', () => {
|
||||
it('formats list fields the same way', () => {
|
||||
expect(orderListRentalTerms({ rental_days: 10, daily_loss_m: 10 })).toEqual({
|
||||
rentalDays: 10,
|
||||
dailyLossText: '10M/天',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -218,6 +218,34 @@ export function getSnapshotHafCoinM(order: Order | null) {
|
||||
return roundQuantity(readNumber(snapshot?.haf_coin_amount) / 1000000)
|
||||
}
|
||||
|
||||
export function formatDailyLossText(dailyLossM: number) {
|
||||
const rounded = Math.round(Number(dailyLossM || 0) * 10) / 10
|
||||
if (rounded <= 0) return ''
|
||||
const text = Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1)
|
||||
return `${text}M/天`
|
||||
}
|
||||
|
||||
export function orderRentalTerms(order: Order | null) {
|
||||
const dailyLossM = readNumber(readAssetSummary(order)?.daily_loss_m)
|
||||
const coinM = getSnapshotHafCoinM(order)
|
||||
if (dailyLossM <= 0 || coinM <= 0) return { rentalDays: 0, dailyLossText: '' }
|
||||
return {
|
||||
rentalDays: Math.max(1, Math.ceil(coinM / dailyLossM)),
|
||||
dailyLossText: formatDailyLossText(dailyLossM),
|
||||
}
|
||||
}
|
||||
|
||||
export function orderListRentalTerms(
|
||||
item?: { rental_days?: number; daily_loss_m?: number } | null
|
||||
) {
|
||||
const rentalDays = Math.ceil(readNumber(item?.rental_days))
|
||||
const dailyLossText = formatDailyLossText(readNumber(item?.daily_loss_m))
|
||||
return {
|
||||
rentalDays: rentalDays > 0 ? rentalDays : 0,
|
||||
dailyLossText,
|
||||
}
|
||||
}
|
||||
|
||||
export function orderEstimatedEndAt(order: Order | null) {
|
||||
if (order?.estimated_end_at) return order.estimated_end_at
|
||||
const rentedAt = order?.rented_at
|
||||
|
||||
@@ -93,7 +93,35 @@
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 14px 0;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.order-title:has(+ .order-terms) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.order-terms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 8px 0 14px;
|
||||
}
|
||||
|
||||
.order-terms span {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.order-terms .term-rental {
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.order-terms .term-loss {
|
||||
background: #f97316;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.order-meta-grid {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useCheckoutSimulator } from '@/features/orders/composables/useCheckoutS
|
||||
import { useOrderActions, CONFIRMABLE_ACTIONS } from '@/features/orders/composables/useOrderActions'
|
||||
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
|
||||
import { useMobilePayWaySelect } from '@/features/orders/composables/useMobilePayWaySelect'
|
||||
import { amountYuan, money } from '@/features/orders/composables/useOrderSnapshot'
|
||||
import { amountYuan, money, orderRentalTerms } from '@/features/orders/composables/useOrderSnapshot'
|
||||
import {
|
||||
OrderAccountSnapshot,
|
||||
OrderCheckoutSummary,
|
||||
@@ -119,6 +119,8 @@ const {
|
||||
onCreateDisputeSuccess: () => (showDisputePopup.value = false),
|
||||
})
|
||||
|
||||
const rentalTerms = computed(() => orderRentalTerms(order.value))
|
||||
|
||||
// Mobile 独有 UI 状态:各类 popup 显隐。
|
||||
const showDisputePopup = ref(false)
|
||||
const showCounterPopup = ref(false)
|
||||
@@ -204,6 +206,14 @@ async function copyListingCode() {
|
||||
商品编号 {{ listingCode }}
|
||||
</button>
|
||||
<h2 class="order-title">{{ order.title }}</h2>
|
||||
<p v-if="rentalTerms.rentalDays || rentalTerms.dailyLossText" class="order-terms">
|
||||
<span v-if="rentalTerms.rentalDays" class="term-rental"
|
||||
>租期 {{ rentalTerms.rentalDays }}天</span
|
||||
>
|
||||
<span v-if="rentalTerms.dailyLossText" class="term-loss"
|
||||
>消耗 {{ rentalTerms.dailyLossText }}</span
|
||||
>
|
||||
</p>
|
||||
<div class="order-meta-grid">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">{{ orderAmountLabel }}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
startOrderPayment,
|
||||
type UserOrderListItem,
|
||||
} from '@/features/orders/api/orders'
|
||||
import { orderListRentalTerms } from '@/features/orders/composables/useOrderSnapshot'
|
||||
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
|
||||
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
|
||||
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
|
||||
@@ -207,6 +208,17 @@ function bizTypeLabel(bizType: BizType) {
|
||||
return bizType === 'crash' ? '撞车' : '租赁'
|
||||
}
|
||||
|
||||
function rentalTermChips(order?: UserOrderListItem | null) {
|
||||
const terms = orderListRentalTerms(order)
|
||||
const chips: Array<{ label: string; value: string; tone: string }> = []
|
||||
if (terms.rentalDays > 0) {
|
||||
chips.push({ label: '租期', value: `${terms.rentalDays}天`, tone: 'term-rental' })
|
||||
}
|
||||
if (terms.dailyLossText)
|
||||
chips.push({ label: '消耗', value: terms.dailyLossText, tone: 'term-loss' })
|
||||
return chips
|
||||
}
|
||||
|
||||
function goDetail(item: UnifiedOrderItem) {
|
||||
if (item.bizType === 'crash') {
|
||||
router.push(`/m/crash/orders/${item.id}`)
|
||||
@@ -381,6 +393,15 @@ async function copyListingCode(order: UserOrderListItem) {
|
||||
商品编号 {{ formatListingCode(item.rental) }}
|
||||
</button>
|
||||
<h3 class="order-title">{{ item.title }}</h3>
|
||||
<p v-if="rentalTermChips(item.rental).length" class="rental-terms">
|
||||
<span
|
||||
v-for="chip in rentalTermChips(item.rental)"
|
||||
:key="chip.label"
|
||||
:class="chip.tone"
|
||||
>
|
||||
{{ chip.label }} {{ chip.value }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="tag-row">
|
||||
<span class="info-tag">{{ item.rental.server_region }}</span>
|
||||
<span class="info-tag">{{ item.rental.login_platform }}</span>
|
||||
@@ -771,6 +792,31 @@ async function copyListingCode(order: UserOrderListItem) {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rental-terms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: -2px 0 8px;
|
||||
}
|
||||
|
||||
.rental-terms span {
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.rental-terms .term-rental {
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rental-terms .term-loss {
|
||||
background: #f97316;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
@@ -30,6 +30,30 @@
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.order-terms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.order-terms span {
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.order-terms .term-rental {
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.order-terms .term-loss {
|
||||
background: #f97316;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.order-meta {
|
||||
color: #64748b;
|
||||
margin: 6px 0 10px;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ChatDotRound, CopyDocument, Loading, Service } from '@element-plus/icon
|
||||
import {
|
||||
amountYuan,
|
||||
money,
|
||||
orderRentalTerms,
|
||||
OrderAccountSnapshot,
|
||||
OrderCheckoutSummary,
|
||||
OrderHandoffTimeline,
|
||||
@@ -121,6 +122,8 @@ const {
|
||||
onCreateDisputeSuccess: closeDisputeDialog,
|
||||
})
|
||||
|
||||
const rentalTerms = computed(() => orderRentalTerms(order.value))
|
||||
|
||||
// 支付弹窗别名:保持 template 字段名不变。
|
||||
const paymentDialogVisible = cashier.paymentPopupVisible
|
||||
const activePayment = cashier.activePayment
|
||||
@@ -294,6 +297,14 @@ watch([() => route.query.focus, order, loading], () => {
|
||||
<div class="header-info">
|
||||
<p class="eyebrow">订单号:{{ order.order_no }}</p>
|
||||
<h1>{{ order.title }}</h1>
|
||||
<p v-if="rentalTerms.rentalDays || rentalTerms.dailyLossText" class="order-terms">
|
||||
<span v-if="rentalTerms.rentalDays" class="term-rental"
|
||||
>租期 {{ rentalTerms.rentalDays }}天</span
|
||||
>
|
||||
<span v-if="rentalTerms.dailyLossText" class="term-loss"
|
||||
>消耗 {{ rentalTerms.dailyLossText }}</span
|
||||
>
|
||||
</p>
|
||||
<p class="order-meta">{{ order.server_region }} / {{ order.login_platform }}</p>
|
||||
<el-button size="small" plain :icon="CopyDocument" @click="copyListingCode">
|
||||
商品编号 {{ listingCode }}
|
||||
|
||||
@@ -358,6 +358,31 @@
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rental-terms {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rental-terms span {
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.rental-terms .term-rental {
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rental-terms .term-loss {
|
||||
background: #f97316;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rental-title,
|
||||
.crash-product-info strong {
|
||||
font-size: 14px;
|
||||
|
||||
@@ -5,7 +5,12 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { CopyDocument, Search } from '@element-plus/icons-vue'
|
||||
|
||||
import { fetchOrders, hideOwnerOrder, type UserOrderListItem } from '@/features/orders'
|
||||
import {
|
||||
fetchOrders,
|
||||
hideOwnerOrder,
|
||||
orderListRentalTerms,
|
||||
type UserOrderListItem,
|
||||
} from '@/features/orders'
|
||||
import {
|
||||
fetchMyMohongOrders,
|
||||
mohongOrderStatusLabel,
|
||||
@@ -234,6 +239,17 @@ async function handlePay(order: UserOrderListItem) {
|
||||
}
|
||||
}
|
||||
|
||||
function rentalTermChips(order?: UserOrderListItem | null) {
|
||||
const terms = orderListRentalTerms(order)
|
||||
const chips: Array<{ label: string; value: string; tone: string }> = []
|
||||
if (terms.rentalDays > 0) {
|
||||
chips.push({ label: '租期', value: `${terms.rentalDays}天`, tone: 'term-rental' })
|
||||
}
|
||||
if (terms.dailyLossText)
|
||||
chips.push({ label: '消耗', value: terms.dailyLossText, tone: 'term-loss' })
|
||||
return chips
|
||||
}
|
||||
|
||||
function orderRole(order: UserOrderListItem) {
|
||||
return order.role === 'owner' ? '号主' : '租客'
|
||||
}
|
||||
@@ -427,6 +443,15 @@ function bizTypeLabel(bizType: BizType) {
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</button>
|
||||
<span class="rental-title">{{ row.title }}</span>
|
||||
<p v-if="rentalTermChips(row.rental).length" class="rental-terms">
|
||||
<span
|
||||
v-for="chip in rentalTermChips(row.rental)"
|
||||
:key="chip.label"
|
||||
:class="chip.tone"
|
||||
>
|
||||
{{ chip.label }} {{ chip.value }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -558,6 +583,15 @@ function bizTypeLabel(bizType: BizType) {
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</button>
|
||||
<div class="card-title">{{ item.title }}</div>
|
||||
<p v-if="rentalTermChips(item.rental).length" class="rental-terms">
|
||||
<span
|
||||
v-for="chip in rentalTermChips(item.rental)"
|
||||
:key="chip.label"
|
||||
:class="chip.tone"
|
||||
>
|
||||
{{ chip.label }} {{ chip.value }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="card-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">{{ amountLabel(item.rental) }}</span>
|
||||
|
||||
Reference in New Issue
Block a user