feat(listings): 订单和账号详情展示可租时长与每日消耗
租客在订单列表和详情标题能看到每个号的租期、消耗。账号详情把这两项拆成独立格子。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -96,6 +96,8 @@ type UserOrderListItemDTO struct {
|
||||
Title string `json:"title"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
RentalDays int `json:"rental_days,omitempty"`
|
||||
DailyLossM float64 `json:"daily_loss_m,omitempty"`
|
||||
DisplayAmountCent int64 `json:"display_amount_cent"`
|
||||
DepositAmountCent int64 `json:"deposit_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 {
|
||||
displayAmountCent = row.OwnerRentAmountCent
|
||||
}
|
||||
rentalDays, dailyLossM := orderListingTerms(row.AccountSnapshot)
|
||||
return UserOrderListItemDTO{
|
||||
ID: row.ID,
|
||||
OrderNo: row.OrderNo,
|
||||
@@ -188,6 +189,8 @@ func (row orderRow) toUserListItem(userID uint64, paymentDeadlineAt *time.Time)
|
||||
Title: row.Title,
|
||||
ServerRegion: row.ServerRegion,
|
||||
LoginPlatform: row.LoginPlatform,
|
||||
RentalDays: rentalDays,
|
||||
DailyLossM: dailyLossM,
|
||||
DisplayAmountCent: displayAmountCent,
|
||||
DepositAmountCent: row.DepositAmountCent,
|
||||
DepositWaivedAmountCent: row.DepositWaivedAmountCent,
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
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) {
|
||||
row := orderRow{
|
||||
RentalOrder: model.RentalOrder{ID: 1, OrderNo: "ORD-001", Status: orderStatusPendingPayment},
|
||||
|
||||
@@ -124,6 +124,20 @@ func orderEstimatedEndAt(order model.RentalOrder) *time.Time {
|
||||
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 {
|
||||
hafCoinM := readOrderSnapshotCoinM(snapshot)
|
||||
if hafCoinM <= 0 {
|
||||
|
||||
@@ -284,14 +284,18 @@
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* 核心资产四大微卡 */
|
||||
/* 核心资产:有币比时五个并排,没有币比时四个并排 */
|
||||
.core-assets-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.core-assets-bar.has-ratio {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.asset-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -312,6 +316,16 @@
|
||||
border-color: #fed7aa;
|
||||
}
|
||||
|
||||
.rental-tile {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
}
|
||||
|
||||
.loss-tile {
|
||||
background: #fff7ed;
|
||||
border-color: #fdba74;
|
||||
}
|
||||
|
||||
.tile-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
@@ -325,6 +339,10 @@
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.core-assets-bar.has-ratio .tile-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.text-orange {
|
||||
color: #ea580c;
|
||||
}
|
||||
@@ -337,32 +355,12 @@
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.tile-subvalues {
|
||||
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;
|
||||
.text-green {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.val-pill.warm {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fef3c7;
|
||||
color: #b45309;
|
||||
.text-warm {
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
/* ---------- 2. 验号图片展台 (Gallery Card) ---------- */
|
||||
@@ -1179,8 +1177,22 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.core-assets-bar {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
.core-assets-bar,
|
||||
.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 {
|
||||
|
||||
@@ -489,8 +489,8 @@ function goBack() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 核心资产四大微卡(完全对齐首页) -->
|
||||
<div class="core-assets-bar">
|
||||
<!-- 核心资产:哈夫币、安全箱、币比、可租、消耗 -->
|
||||
<div class="core-assets-bar" :class="{ 'has-ratio': ratioDisplay }">
|
||||
<!-- 哈夫币 -->
|
||||
<div class="asset-tile coin-tile">
|
||||
<span class="tile-label">哈夫币资产</span>
|
||||
@@ -513,13 +513,16 @@ function goBack() {
|
||||
<strong class="tile-value text-amber">{{ ratioDisplay }}</strong>
|
||||
</div>
|
||||
|
||||
<!-- 租期与日耗 -->
|
||||
<div class="asset-tile rental-tile">
|
||||
<span class="tile-label">租用与损耗</span>
|
||||
<div class="tile-subvalues">
|
||||
<span class="val-pill green">可租 {{ rentalDuration }}</span>
|
||||
<span v-if="dailyLoss" class="val-pill warm">消耗 {{ dailyLoss }}/天</span>
|
||||
<span class="tile-label">可租</span>
|
||||
<strong class="tile-value text-green">{{ rentalDuration }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="asset-tile loss-tile">
|
||||
<span class="tile-label">消耗</span>
|
||||
<strong class="tile-value text-warm">{{
|
||||
dailyLoss ? `${dailyLoss}/天` : '--'
|
||||
}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -262,14 +262,27 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 核心资产四大微卡 (2x2 网格对齐移动端体验) */
|
||||
/* 核心资产:有币比时上三下二,没有币比时两列 */
|
||||
.core-assets-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -290,6 +303,16 @@
|
||||
border-color: #fed7aa;
|
||||
}
|
||||
|
||||
.rental-tile {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
}
|
||||
|
||||
.loss-tile {
|
||||
background: #fff7ed;
|
||||
border-color: #fdba74;
|
||||
}
|
||||
|
||||
.tile-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -303,6 +326,10 @@
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.core-assets-bar.has-ratio .tile-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.text-orange {
|
||||
color: #ea580c;
|
||||
}
|
||||
@@ -311,32 +338,12 @@
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.tile-subvalues {
|
||||
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;
|
||||
.text-green {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.val-pill.warm {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fef3c7;
|
||||
color: #b45309;
|
||||
.text-warm {
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
/* ========== 2. 验号多图互动展台 (Gallery Stage) ========== */
|
||||
|
||||
@@ -466,8 +466,8 @@ function goBack() {
|
||||
{{ getListingSubtitle(listing) }}
|
||||
</p>
|
||||
|
||||
<!-- 核心资产四大微卡(完全对标 PC 详情页与首页标准) -->
|
||||
<div class="core-assets-bar">
|
||||
<!-- 核心资产:哈夫币、安全箱、币比、可租、消耗 -->
|
||||
<div class="core-assets-bar" :class="{ 'has-ratio': ratioDisplay }">
|
||||
<!-- 哈夫币 -->
|
||||
<div class="asset-tile coin-tile">
|
||||
<span class="tile-label">哈夫币资产</span>
|
||||
@@ -490,13 +490,14 @@ function goBack() {
|
||||
<strong class="tile-value text-slate">{{ ratioDisplay }}</strong>
|
||||
</div>
|
||||
|
||||
<!-- 租用与损耗 -->
|
||||
<div class="asset-tile rental-tile">
|
||||
<span class="tile-label">租用与损耗</span>
|
||||
<div class="tile-subvalues">
|
||||
<span class="val-pill green">可租 {{ rentalDuration }}</span>
|
||||
<span v-if="dailyLoss" class="val-pill warm">消耗 {{ dailyLoss }}/天</span>
|
||||
<span class="tile-label">可租</span>
|
||||
<strong class="tile-value text-green">{{ rentalDuration }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="asset-tile loss-tile">
|
||||
<span class="tile-label">消耗</span>
|
||||
<strong class="tile-value text-warm">{{ dailyLoss ? `${dailyLoss}/天` : '--' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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