diff --git a/frontend/src/utils/listingDisplay.ts b/frontend/src/utils/listingDisplay.ts index fc28d30..3403b49 100644 --- a/frontend/src/utils/listingDisplay.ts +++ b/frontend/src/utils/listingDisplay.ts @@ -8,8 +8,10 @@ export interface ListingDisplayChip { export interface ListingDisplayResource { key: string; label: string; + price: string; quantity: number; mode: string; + amount: number; } export function getCoinWan(item: Listing) { @@ -34,6 +36,18 @@ export function getListingDisplayPrice(item: Listing) { return Number(item.price || 0); } +export function getListingRentPrice(item: Listing) { + const buyerCoinBasePrice = readPriceBreakdownNumber(item, "buyer_coin_base_price"); + if (buyerCoinBasePrice > 0) return Math.round(buyerCoinBasePrice); + return Math.max(0, Math.round(getListingDisplayPrice(item) - getListingConsumablePrice(item))); +} + +export function getListingConsumablePrice(item: Listing) { + const consumablePrice = readPriceBreakdownNumber(item, "consumable_price"); + if (consumablePrice > 0) return Math.round(consumablePrice); + return getListingResources(item).reduce((sum, resource) => sum + resource.amount, 0); +} + export function getListingSellerPrice(item: Listing) { const priceBreakdown = item.asset_summary?.price_breakdown; if (typeof priceBreakdown === "object" && priceBreakdown !== null) { @@ -118,8 +132,13 @@ export function getListingResources(item: Listing): ListingDisplayResource[] { return { key: typeof row.key === "string" ? row.key : "", label: typeof row.label === "string" ? row.label : "", + price: typeof row.price === "string" ? row.price : "", quantity: readUnknownNumber(row.quantity), mode: typeof row.mode === "string" ? row.mode : "", + amount: + row.mode === "收费" + ? Math.round(readUnknownNumber(row.quantity) * readUnitPrice(typeof row.price === "string" ? row.price : "")) + : 0, }; }) .filter((resource): resource is ListingDisplayResource => { @@ -213,8 +232,7 @@ export function getEstimatedRentalDays(item: Listing) { export function formatEstimatedRentalDuration(item: Listing) { const days = getEstimatedRentalDays(item); if (days <= 0) return "--"; - if (days < 1) return `约${Math.max(1, Math.round(days * 24))}小时`; - return `约${formatCompactNumber(days)}天`; + return `${Math.max(1, Math.round(days))}天`; } export function readAssetString(item: Listing, key: string) { @@ -235,6 +253,24 @@ function readUnknownNumber(value: unknown) { return 0; } +function readPriceBreakdownNumber(item: Listing, key: string) { + const priceBreakdown = item.asset_summary?.price_breakdown; + if (typeof priceBreakdown !== "object" || priceBreakdown === null) return 0; + return readUnknownNumber((priceBreakdown as Record)[key]); +} + +function readUnitPrice(priceText: string) { + const normalized = priceText.replace(/,/g, ",").trim(); + const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/); + if (fractionMatch) { + const amount = Number(fractionMatch[1]); + const count = Number(fractionMatch[2]); + return count > 0 ? amount / count : 0; + } + const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/); + return singleMatch ? Number(singleMatch[1]) : 0; +} + function formatInsuranceSlotText(value: string) { const parts = value.split("*").map((item) => Number(item)); const rows = parts[0] || 0; diff --git a/frontend/src/views/mobile/MobileListingDetailView.vue b/frontend/src/views/mobile/MobileListingDetailView.vue index d089e77..5d8fb19 100644 --- a/frontend/src/views/mobile/MobileListingDetailView.vue +++ b/frontend/src/views/mobile/MobileListingDetailView.vue @@ -12,14 +12,15 @@ import { formatRatio, getCoinWan, getDailyLoss, + getListingConsumablePrice, getListingChips, getListingDisplayPrice, + getListingRentPrice, getListingResources, getListingSubtitle, getListingTitle, getLoginMethod, getServerRegion, - readAssetNumber, readAssetString, } from "@/utils/listingDisplay"; @@ -46,14 +47,27 @@ const orderTotal = computed(() => { return `${Math.round(getListingDisplayPrice(listing.value))}`; }); +const orderPriceBreakdown = computed(() => { + if (!listing.value) { + return { + rent: 0, + consumable: 0, + }; + } + return { + rent: getListingRentPrice(listing.value), + consumable: getListingConsumablePrice(listing.value), + }; +}); + const detailMetrics = computed(() => { if (!listing.value) return []; - const totalAssetWan = readAssetNumber(listing.value, "total_asset_wan"); + const dailyLoss = getDailyLoss(listing.value); return [ { label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" }, { - label: "总资产", - value: totalAssetWan > 0 ? formatHafCoinM(totalAssetWan) : "--", + label: "日损耗", + value: dailyLoss ? `${dailyLoss}/天` : "--", tone: "coin", }, { label: "价格", value: `¥${getListingDisplayPrice(listing.value)}`, tone: "price" }, @@ -215,14 +229,6 @@ function isNavActive(path: string) { {{ listing.rank_level }} - - 损耗 {{ getDailyLoss(listing) }} -

{{ getListingTitle(listing) }}

{{ getListingSubtitle(listing) }}

@@ -271,7 +277,12 @@ function isNavActive(path: string) { > {{ resource.label }} {{ resource.quantity }} - {{ resource.mode }} + + {{ resource.mode || "--" }} + ¥{{ resource.amount }} + {{ resource.price || "¥0" }} + 无额外收费 + @@ -312,6 +323,10 @@ function isNavActive(path: string) { 价格 ¥{{ orderTotal }} +
+ 租金 ¥{{ orderPriceBreakdown.rent }} + 额外 ¥{{ orderPriceBreakdown.consumable }} +
{ return `${Math.round(getListingDisplayPrice(listing.value))}`; }); +const orderPriceBreakdown = computed(() => { + if (!listing.value) { + return { + rent: 0, + consumable: 0, + total: 0, + }; + } + return { + rent: getListingRentPrice(listing.value), + consumable: getListingConsumablePrice(listing.value), + total: Math.round(getListingDisplayPrice(listing.value)), + }; +}); + const coverURL = computed(() => { if (!listing.value) return ""; return listing.value.cover_url || listing.value.screenshot_urls?.[0] || ""; @@ -53,12 +70,12 @@ const coverURL = computed(() => { const detailMetrics = computed(() => { if (!listing.value) return []; - const totalAssetWan = readAssetNumber(listing.value, "total_asset_wan"); + const dailyLoss = getDailyLoss(listing.value); return [ { label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" }, { - label: "总资产", - value: totalAssetWan > 0 ? formatHafCoinM(totalAssetWan) : "--", + label: "日损耗", + value: dailyLoss ? `${dailyLoss}/天` : "--", tone: "coin", }, { label: "价格", value: `¥${orderTotal.value}`, tone: "price" }, @@ -167,7 +184,6 @@ function listingPrice(item: Listing) { {{ getServerRegion(listing) }} {{ getLoginMethod(listing) }} {{ listing.rank_level }} - 损耗 {{ getDailyLoss(listing) }}

{{ getListingTitle(listing) }}

{{ getListingSubtitle(listing) }}

@@ -209,7 +225,12 @@ function listingPrice(item: Listing) {
{{ resource.label }} {{ resource.quantity }} - {{ resource.mode }} + + {{ resource.mode || "--" }} + ¥{{ resource.amount }} + {{ resource.price || "¥0" }} + 无额外收费 +
@@ -256,9 +277,21 @@ function listingPrice(item: Listing) {

平台托管订单与押金,按平台交接流程完成账号使用。

- 价格 - ¥{{ listingPrice(listing) }} - 押金 ¥{{ listing.deposit_amount }} +
+ 租赁价格 + ¥{{ listingPrice(listing) }} +
+
+
+ 基础租金 + ¥{{ orderPriceBreakdown.rent }} +
+
+ 额外物品 + ¥{{ orderPriceBreakdown.consumable }} +
+
+ 押金另付 ¥{{ listing.deposit_amount }}
@@ -520,13 +553,25 @@ function listingPrice(item: Listing) { } .detail-resource-card em { + display: flex; + align-items: center; + justify-content: space-between; grid-column: 1 / -1; - color: #ff6a00; font-size: 12px; font-style: normal; font-weight: 900; } +.detail-resource-card em b { + color: #ff6a00; +} + +.detail-resource-card em small { + color: #17233d; + font-size: 13px; + font-weight: 900; +} + .skin-groups { display: grid; gap: 16px; @@ -580,6 +625,62 @@ function listingPrice(item: Listing) { margin-bottom: 14px; } +.order-total-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(217, 119, 6, 0.14); +} + +.order-total-head span { + color: #92400e; + font-size: 14px; + font-weight: 900; +} + +.order-total-head strong { + margin: 0; + color: #ef4444; + font-size: 32px; + line-height: 1; +} + +.order-price-breakdown { + display: grid; + gap: 8px; + padding: 12px 0; +} + +.order-price-breakdown div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.order-price-breakdown span { + color: #8a5a12; + font-size: 13px; + font-weight: 900; +} + +.order-price-breakdown strong { + margin: 0; + color: #17233d; + font-size: 16px; + line-height: 1.2; +} + +.order-total-box > em { + padding-top: 10px; + border-top: 1px solid rgba(217, 119, 6, 0.14); + color: #92400e; + font-size: 13px; + font-weight: 900; +} + .order-check-list { display: grid; gap: 10px;