feat(listings): 订单和账号详情展示可租时长与每日消耗

租客在订单列表和详情标题能看到每个号的租期、消耗。账号详情把这两项拆成独立格子。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-10-05 16:32:08 +08:00
co-authored by Cursor
parent c559287ee4
commit f2060c19cf
18 changed files with 393 additions and 71 deletions
+2
View File
@@ -96,6 +96,8 @@ type UserOrderListItemDTO struct {
Title string `json:"title"` Title string `json:"title"`
ServerRegion string `json:"server_region"` ServerRegion string `json:"server_region"`
LoginPlatform string `json:"login_platform"` LoginPlatform string `json:"login_platform"`
RentalDays int `json:"rental_days,omitempty"`
DailyLossM float64 `json:"daily_loss_m,omitempty"`
DisplayAmountCent int64 `json:"display_amount_cent"` DisplayAmountCent int64 `json:"display_amount_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"` DepositAmountCent int64 `json:"deposit_amount_cent"`
DepositWaivedAmountCent int64 `json:"deposit_waived_amount_cent"` DepositWaivedAmountCent int64 `json:"deposit_waived_amount_cent"`
@@ -179,6 +179,7 @@ func (row orderRow) toUserListItem(userID uint64, paymentDeadlineAt *time.Time)
if role == "owner" && row.OwnerRentAmountCent > 0 { if role == "owner" && row.OwnerRentAmountCent > 0 {
displayAmountCent = row.OwnerRentAmountCent displayAmountCent = row.OwnerRentAmountCent
} }
rentalDays, dailyLossM := orderListingTerms(row.AccountSnapshot)
return UserOrderListItemDTO{ return UserOrderListItemDTO{
ID: row.ID, ID: row.ID,
OrderNo: row.OrderNo, OrderNo: row.OrderNo,
@@ -188,6 +189,8 @@ func (row orderRow) toUserListItem(userID uint64, paymentDeadlineAt *time.Time)
Title: row.Title, Title: row.Title,
ServerRegion: row.ServerRegion, ServerRegion: row.ServerRegion,
LoginPlatform: row.LoginPlatform, LoginPlatform: row.LoginPlatform,
RentalDays: rentalDays,
DailyLossM: dailyLossM,
DisplayAmountCent: displayAmountCent, DisplayAmountCent: displayAmountCent,
DepositAmountCent: row.DepositAmountCent, DepositAmountCent: row.DepositAmountCent,
DepositWaivedAmountCent: row.DepositWaivedAmountCent, DepositWaivedAmountCent: row.DepositWaivedAmountCent,
@@ -7,6 +7,8 @@ import (
"time" "time"
"hfb_sys/backend/internal/model" "hfb_sys/backend/internal/model"
"gorm.io/datatypes"
) )
func TestOrderDTOForUserHidesDepositHoldFields(t *testing.T) { func TestOrderDTOForUserHidesDepositHoldFields(t *testing.T) {
@@ -72,6 +74,44 @@ func TestUserOrderListItemDoesNotSerializeDetailFields(t *testing.T) {
} }
} }
func TestUserOrderListItemIncludesRentalTerms(t *testing.T) {
row := orderRow{
RentalOrder: model.RentalOrder{
AccountSnapshot: datatypes.JSON([]byte(`{"haf_coin_amount":100000000,"asset_summary":{"daily_loss_m":10}}`)),
},
Title: "微信 三角洲巅峰 100M哈夫币",
}
item := row.toUserListItem(20, nil)
if item.RentalDays != 10 {
t.Fatalf("rental days = %d, want 10", item.RentalDays)
}
if item.DailyLossM != 10 {
t.Fatalf("daily loss = %v, want 10", item.DailyLossM)
}
raw, err := json.Marshal(item)
if err != nil {
t.Fatalf("Marshal() error = %v", err)
}
text := string(raw)
if !strings.Contains(text, `"rental_days":10`) || !strings.Contains(text, `"daily_loss_m":10`) {
t.Fatalf("list response missing rental terms: %s", text)
}
}
func TestUserOrderListItemOmitsRentalTermsWithoutSnapshot(t *testing.T) {
item := orderRow{Title: "测试账号"}.toUserListItem(20, nil)
raw, err := json.Marshal(item)
if err != nil {
t.Fatalf("Marshal() error = %v", err)
}
text := string(raw)
if strings.Contains(text, "rental_days") || strings.Contains(text, "daily_loss_m") {
t.Fatalf("empty snapshot should omit rental terms: %s", text)
}
}
func TestAdminOrderListItemShowsPhonesAndOmitsSnapshot(t *testing.T) { func TestAdminOrderListItemShowsPhonesAndOmitsSnapshot(t *testing.T) {
row := orderRow{ row := orderRow{
RentalOrder: model.RentalOrder{ID: 1, OrderNo: "ORD-001", Status: orderStatusPendingPayment}, RentalOrder: model.RentalOrder{ID: 1, OrderNo: "ORD-001", Status: orderStatusPendingPayment},
+14
View File
@@ -124,6 +124,20 @@ func orderEstimatedEndAt(order model.RentalOrder) *time.Time {
return &endAt return &endAt
} }
// orderListingTerms 用下单快照里的哈夫币和日损耗估算租期,和首页「可租 / 消耗」同一口径。
func orderListingTerms(snapshot datatypes.JSON) (days int, dailyLossM float64) {
dailyLossM = readOrderSnapshotAssetNumber(snapshot, "daily_loss_m")
coinM := readOrderSnapshotCoinM(snapshot)
if dailyLossM <= 0 || coinM <= 0 {
return 0, 0
}
days = int(math.Ceil(coinM / dailyLossM))
if days < 1 {
days = 1
}
return days, roundQuantity(dailyLossM)
}
func estimateOrderDurationHours(snapshot datatypes.JSON) int { func estimateOrderDurationHours(snapshot datatypes.JSON) int {
hafCoinM := readOrderSnapshotCoinM(snapshot) hafCoinM := readOrderSnapshotCoinM(snapshot)
if hafCoinM <= 0 { if hafCoinM <= 0 {
@@ -284,14 +284,18 @@
color: #64748b; color: #64748b;
} }
/* 核心资产四大微卡 */ /* 核心资产:有币比时五个并排,没有币比时四个并排 */
.core-assets-bar { .core-assets-bar {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px; gap: 12px;
margin-top: 18px; margin-top: 18px;
} }
.core-assets-bar.has-ratio {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.asset-tile { .asset-tile {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -312,6 +316,16 @@
border-color: #fed7aa; border-color: #fed7aa;
} }
.rental-tile {
background: #f0fdf4;
border-color: #86efac;
}
.loss-tile {
background: #fff7ed;
border-color: #fdba74;
}
.tile-label { .tile-label {
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
@@ -325,6 +339,10 @@
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
.core-assets-bar.has-ratio .tile-value {
font-size: 20px;
}
.text-orange { .text-orange {
color: #ea580c; color: #ea580c;
} }
@@ -337,32 +355,12 @@
color: #475569; color: #475569;
} }
.tile-subvalues { .text-green {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.val-pill {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 700;
}
.val-pill.green {
background: #f0fdf4;
border: 1px solid #dcfce7;
color: #15803d; color: #15803d;
} }
.val-pill.warm { .text-warm {
background: #fffbeb; color: #c2410c;
border: 1px solid #fef3c7;
color: #b45309;
} }
/* ---------- 2. 验号图片展台 (Gallery Card) ---------- */ /* ---------- 2. 验号图片展台 (Gallery Card) ---------- */
@@ -1179,8 +1177,22 @@
align-items: flex-start; align-items: flex-start;
} }
.core-assets-bar { .core-assets-bar,
grid-template-columns: repeat(2, 1fr); .core-assets-bar.has-ratio {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.core-assets-bar .asset-tile {
grid-column: span 3;
}
.core-assets-bar.has-ratio .asset-tile {
grid-column: span 2;
}
.core-assets-bar.has-ratio .rental-tile,
.core-assets-bar.has-ratio .loss-tile {
grid-column: span 3;
} }
.specs-grid { .specs-grid {
@@ -489,8 +489,8 @@ function goBack() {
</p> </p>
</div> </div>
<!-- 核心资产四大微卡(完全对齐首页) --> <!-- 核心资产:哈夫币、安全箱、币比、可租、消耗 -->
<div class="core-assets-bar"> <div class="core-assets-bar" :class="{ 'has-ratio': ratioDisplay }">
<!-- 哈夫币 --> <!-- 哈夫币 -->
<div class="asset-tile coin-tile"> <div class="asset-tile coin-tile">
<span class="tile-label">哈夫币资产</span> <span class="tile-label">哈夫币资产</span>
@@ -513,13 +513,16 @@ function goBack() {
<strong class="tile-value text-amber">{{ ratioDisplay }}</strong> <strong class="tile-value text-amber">{{ ratioDisplay }}</strong>
</div> </div>
<!-- 租期与日耗 -->
<div class="asset-tile rental-tile"> <div class="asset-tile rental-tile">
<span class="tile-label">租用与损耗</span> <span class="tile-label">可租</span>
<div class="tile-subvalues"> <strong class="tile-value text-green">{{ rentalDuration }}</strong>
<span class="val-pill green">可租 {{ rentalDuration }}</span> </div>
<span v-if="dailyLoss" class="val-pill warm">消耗 {{ dailyLoss }}/天</span>
</div> <div class="asset-tile loss-tile">
<span class="tile-label">消耗</span>
<strong class="tile-value text-warm">{{
dailyLoss ? `${dailyLoss}/天` : '--'
}}</strong>
</div> </div>
</div> </div>
</section> </section>
@@ -262,14 +262,27 @@
line-height: 1.5; line-height: 1.5;
} }
/* 核心资产四大微卡 (2x2 网格对齐移动端体验) */ /* 核心资产:有币比时上三下二,没有币比时两列 */
.core-assets-bar { .core-assets-bar {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px; gap: 8px;
margin-top: 10px; margin-top: 10px;
} }
.core-assets-bar.has-ratio {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.core-assets-bar.has-ratio .asset-tile {
grid-column: span 2;
}
.core-assets-bar.has-ratio .rental-tile,
.core-assets-bar.has-ratio .loss-tile {
grid-column: span 3;
}
.asset-tile { .asset-tile {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -290,6 +303,16 @@
border-color: #fed7aa; border-color: #fed7aa;
} }
.rental-tile {
background: #f0fdf4;
border-color: #86efac;
}
.loss-tile {
background: #fff7ed;
border-color: #fdba74;
}
.tile-label { .tile-label {
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
@@ -303,6 +326,10 @@
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
.core-assets-bar.has-ratio .tile-value {
font-size: 16px;
}
.text-orange { .text-orange {
color: #ea580c; color: #ea580c;
} }
@@ -311,32 +338,12 @@
color: #0f172a; color: #0f172a;
} }
.tile-subvalues { .text-green {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.val-pill {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
}
.val-pill.green {
background: #f0fdf4;
border: 1px solid #dcfce7;
color: #15803d; color: #15803d;
} }
.val-pill.warm { .text-warm {
background: #fffbeb; color: #c2410c;
border: 1px solid #fef3c7;
color: #b45309;
} }
/* ========== 2. 验号多图互动展台 (Gallery Stage) ========== */ /* ========== 2. 验号多图互动展台 (Gallery Stage) ========== */
@@ -466,8 +466,8 @@ function goBack() {
{{ getListingSubtitle(listing) }} {{ getListingSubtitle(listing) }}
</p> </p>
<!-- 核心资产四大微卡(完全对标 PC 详情页与首页标准) --> <!-- 核心资产:哈夫币、安全箱、币比、可租、消耗 -->
<div class="core-assets-bar"> <div class="core-assets-bar" :class="{ 'has-ratio': ratioDisplay }">
<!-- 哈夫币 --> <!-- 哈夫币 -->
<div class="asset-tile coin-tile"> <div class="asset-tile coin-tile">
<span class="tile-label">哈夫币资产</span> <span class="tile-label">哈夫币资产</span>
@@ -490,13 +490,14 @@ function goBack() {
<strong class="tile-value text-slate">{{ ratioDisplay }}</strong> <strong class="tile-value text-slate">{{ ratioDisplay }}</strong>
</div> </div>
<!-- 租用与损耗 -->
<div class="asset-tile rental-tile"> <div class="asset-tile rental-tile">
<span class="tile-label">租用与损耗</span> <span class="tile-label">可租</span>
<div class="tile-subvalues"> <strong class="tile-value text-green">{{ rentalDuration }}</strong>
<span class="val-pill green">可租 {{ rentalDuration }}</span> </div>
<span v-if="dailyLoss" class="val-pill warm">消耗 {{ dailyLoss }}/天</span>
</div> <div class="asset-tile loss-tile">
<span class="tile-label">消耗</span>
<strong class="tile-value text-warm">{{ dailyLoss ? `${dailyLoss}/天` : '--' }}</strong>
</div> </div>
</div> </div>
</div> </div>
@@ -89,6 +89,8 @@ export interface UserOrderListItem {
title: string title: string
server_region: string server_region: string
login_platform: string login_platform: string
rental_days?: number
daily_loss_m?: number
display_amount_cent: number display_amount_cent: number
deposit_amount_cent: number deposit_amount_cent: number
deposit_waived_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) 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) { export function orderEstimatedEndAt(order: Order | null) {
if (order?.estimated_end_at) return order.estimated_end_at if (order?.estimated_end_at) return order.estimated_end_at
const rentedAt = order?.rented_at const rentedAt = order?.rented_at
@@ -93,7 +93,35 @@
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
color: #1a1a1a; 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 { .order-meta-grid {
@@ -10,7 +10,7 @@ import { useCheckoutSimulator } from '@/features/orders/composables/useCheckoutS
import { useOrderActions, CONFIRMABLE_ACTIONS } from '@/features/orders/composables/useOrderActions' import { useOrderActions, CONFIRMABLE_ACTIONS } from '@/features/orders/composables/useOrderActions'
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier' import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
import { useMobilePayWaySelect } from '@/features/orders/composables/useMobilePayWaySelect' 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 { import {
OrderAccountSnapshot, OrderAccountSnapshot,
OrderCheckoutSummary, OrderCheckoutSummary,
@@ -119,6 +119,8 @@ const {
onCreateDisputeSuccess: () => (showDisputePopup.value = false), onCreateDisputeSuccess: () => (showDisputePopup.value = false),
}) })
const rentalTerms = computed(() => orderRentalTerms(order.value))
// Mobile 独有 UI 状态:各类 popup 显隐。 // Mobile 独有 UI 状态:各类 popup 显隐。
const showDisputePopup = ref(false) const showDisputePopup = ref(false)
const showCounterPopup = ref(false) const showCounterPopup = ref(false)
@@ -204,6 +206,14 @@ async function copyListingCode() {
商品编号 {{ listingCode }} 商品编号 {{ listingCode }}
</button> </button>
<h2 class="order-title">{{ order.title }}</h2> <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="order-meta-grid">
<div class="meta-item"> <div class="meta-item">
<span class="meta-label">{{ orderAmountLabel }}</span> <span class="meta-label">{{ orderAmountLabel }}</span>
@@ -11,6 +11,7 @@ import {
startOrderPayment, startOrderPayment,
type UserOrderListItem, type UserOrderListItem,
} from '@/features/orders/api/orders' } from '@/features/orders/api/orders'
import { orderListRentalTerms } from '@/features/orders/composables/useOrderSnapshot'
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue' import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue' import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier' import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
@@ -207,6 +208,17 @@ function bizTypeLabel(bizType: BizType) {
return bizType === 'crash' ? '撞车' : '租赁' 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) { function goDetail(item: UnifiedOrderItem) {
if (item.bizType === 'crash') { if (item.bizType === 'crash') {
router.push(`/m/crash/orders/${item.id}`) router.push(`/m/crash/orders/${item.id}`)
@@ -381,6 +393,15 @@ async function copyListingCode(order: UserOrderListItem) {
商品编号 {{ formatListingCode(item.rental) }} 商品编号 {{ formatListingCode(item.rental) }}
</button> </button>
<h3 class="order-title">{{ item.title }}</h3> <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"> <div class="tag-row">
<span class="info-tag">{{ item.rental.server_region }}</span> <span class="info-tag">{{ item.rental.server_region }}</span>
<span class="info-tag">{{ item.rental.login_platform }}</span> <span class="info-tag">{{ item.rental.login_platform }}</span>
@@ -771,6 +792,31 @@ async function copyListingCode(order: UserOrderListItem) {
line-height: 1.4; 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 { .tag-row {
display: flex; display: flex;
gap: 6px; gap: 6px;
@@ -30,6 +30,30 @@
line-height: 1.25; 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 { .order-meta {
color: #64748b; color: #64748b;
margin: 6px 0 10px; margin: 6px 0 10px;
@@ -7,6 +7,7 @@ import { ChatDotRound, CopyDocument, Loading, Service } from '@element-plus/icon
import { import {
amountYuan, amountYuan,
money, money,
orderRentalTerms,
OrderAccountSnapshot, OrderAccountSnapshot,
OrderCheckoutSummary, OrderCheckoutSummary,
OrderHandoffTimeline, OrderHandoffTimeline,
@@ -121,6 +122,8 @@ const {
onCreateDisputeSuccess: closeDisputeDialog, onCreateDisputeSuccess: closeDisputeDialog,
}) })
const rentalTerms = computed(() => orderRentalTerms(order.value))
// 支付弹窗别名:保持 template 字段名不变。 // 支付弹窗别名:保持 template 字段名不变。
const paymentDialogVisible = cashier.paymentPopupVisible const paymentDialogVisible = cashier.paymentPopupVisible
const activePayment = cashier.activePayment const activePayment = cashier.activePayment
@@ -294,6 +297,14 @@ watch([() => route.query.focus, order, loading], () => {
<div class="header-info"> <div class="header-info">
<p class="eyebrow">订单号:{{ order.order_no }}</p> <p class="eyebrow">订单号:{{ order.order_no }}</p>
<h1>{{ order.title }}</h1> <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> <p class="order-meta">{{ order.server_region }} / {{ order.login_platform }}</p>
<el-button size="small" plain :icon="CopyDocument" @click="copyListingCode"> <el-button size="small" plain :icon="CopyDocument" @click="copyListingCode">
商品编号 {{ listingCode }} 商品编号 {{ listingCode }}
@@ -358,6 +358,31 @@
margin-bottom: 2px; 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, .rental-title,
.crash-product-info strong { .crash-product-info strong {
font-size: 14px; font-size: 14px;
@@ -5,7 +5,12 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { CopyDocument, Search } from '@element-plus/icons-vue' 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 { import {
fetchMyMohongOrders, fetchMyMohongOrders,
mohongOrderStatusLabel, 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) { function orderRole(order: UserOrderListItem) {
return order.role === 'owner' ? '号主' : '租客' return order.role === 'owner' ? '号主' : '租客'
} }
@@ -427,6 +443,15 @@ function bizTypeLabel(bizType: BizType) {
<el-icon><CopyDocument /></el-icon> <el-icon><CopyDocument /></el-icon>
</button> </button>
<span class="rental-title">{{ row.title }}</span> <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> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -558,6 +583,15 @@ function bizTypeLabel(bizType: BizType) {
<el-icon><CopyDocument /></el-icon> <el-icon><CopyDocument /></el-icon>
</button> </button>
<div class="card-title">{{ item.title }}</div> <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="card-meta">
<div class="meta-row"> <div class="meta-row">
<span class="meta-label">{{ amountLabel(item.rental) }}</span> <span class="meta-label">{{ amountLabel(item.rental) }}</span>