优化租号的详情界面
This commit is contained in:
@@ -8,8 +8,10 @@ export interface ListingDisplayChip {
|
|||||||
export interface ListingDisplayResource {
|
export interface ListingDisplayResource {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
price: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
mode: string;
|
mode: string;
|
||||||
|
amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCoinWan(item: Listing) {
|
export function getCoinWan(item: Listing) {
|
||||||
@@ -34,6 +36,18 @@ export function getListingDisplayPrice(item: Listing) {
|
|||||||
return Number(item.price || 0);
|
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) {
|
export function getListingSellerPrice(item: Listing) {
|
||||||
const priceBreakdown = item.asset_summary?.price_breakdown;
|
const priceBreakdown = item.asset_summary?.price_breakdown;
|
||||||
if (typeof priceBreakdown === "object" && priceBreakdown !== null) {
|
if (typeof priceBreakdown === "object" && priceBreakdown !== null) {
|
||||||
@@ -118,8 +132,13 @@ export function getListingResources(item: Listing): ListingDisplayResource[] {
|
|||||||
return {
|
return {
|
||||||
key: typeof row.key === "string" ? row.key : "",
|
key: typeof row.key === "string" ? row.key : "",
|
||||||
label: typeof row.label === "string" ? row.label : "",
|
label: typeof row.label === "string" ? row.label : "",
|
||||||
|
price: typeof row.price === "string" ? row.price : "",
|
||||||
quantity: readUnknownNumber(row.quantity),
|
quantity: readUnknownNumber(row.quantity),
|
||||||
mode: typeof row.mode === "string" ? row.mode : "",
|
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 => {
|
.filter((resource): resource is ListingDisplayResource => {
|
||||||
@@ -213,8 +232,7 @@ export function getEstimatedRentalDays(item: Listing) {
|
|||||||
export function formatEstimatedRentalDuration(item: Listing) {
|
export function formatEstimatedRentalDuration(item: Listing) {
|
||||||
const days = getEstimatedRentalDays(item);
|
const days = getEstimatedRentalDays(item);
|
||||||
if (days <= 0) return "--";
|
if (days <= 0) return "--";
|
||||||
if (days < 1) return `约${Math.max(1, Math.round(days * 24))}小时`;
|
return `${Math.max(1, Math.round(days))}天`;
|
||||||
return `约${formatCompactNumber(days)}天`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readAssetString(item: Listing, key: string) {
|
export function readAssetString(item: Listing, key: string) {
|
||||||
@@ -235,6 +253,24 @@ function readUnknownNumber(value: unknown) {
|
|||||||
return 0;
|
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<string, unknown>)[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) {
|
function formatInsuranceSlotText(value: string) {
|
||||||
const parts = value.split("*").map((item) => Number(item));
|
const parts = value.split("*").map((item) => Number(item));
|
||||||
const rows = parts[0] || 0;
|
const rows = parts[0] || 0;
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ import {
|
|||||||
formatRatio,
|
formatRatio,
|
||||||
getCoinWan,
|
getCoinWan,
|
||||||
getDailyLoss,
|
getDailyLoss,
|
||||||
|
getListingConsumablePrice,
|
||||||
getListingChips,
|
getListingChips,
|
||||||
getListingDisplayPrice,
|
getListingDisplayPrice,
|
||||||
|
getListingRentPrice,
|
||||||
getListingResources,
|
getListingResources,
|
||||||
getListingSubtitle,
|
getListingSubtitle,
|
||||||
getListingTitle,
|
getListingTitle,
|
||||||
getLoginMethod,
|
getLoginMethod,
|
||||||
getServerRegion,
|
getServerRegion,
|
||||||
readAssetNumber,
|
|
||||||
readAssetString,
|
readAssetString,
|
||||||
} from "@/utils/listingDisplay";
|
} from "@/utils/listingDisplay";
|
||||||
|
|
||||||
@@ -46,14 +47,27 @@ const orderTotal = computed(() => {
|
|||||||
return `${Math.round(getListingDisplayPrice(listing.value))}`;
|
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(() => {
|
const detailMetrics = computed(() => {
|
||||||
if (!listing.value) return [];
|
if (!listing.value) return [];
|
||||||
const totalAssetWan = readAssetNumber(listing.value, "total_asset_wan");
|
const dailyLoss = getDailyLoss(listing.value);
|
||||||
return [
|
return [
|
||||||
{ label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" },
|
{ label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" },
|
||||||
{
|
{
|
||||||
label: "总资产",
|
label: "日损耗",
|
||||||
value: totalAssetWan > 0 ? formatHafCoinM(totalAssetWan) : "--",
|
value: dailyLoss ? `${dailyLoss}/天` : "--",
|
||||||
tone: "coin",
|
tone: "coin",
|
||||||
},
|
},
|
||||||
{ label: "价格", value: `¥${getListingDisplayPrice(listing.value)}`, tone: "price" },
|
{ label: "价格", value: `¥${getListingDisplayPrice(listing.value)}`, tone: "price" },
|
||||||
@@ -215,14 +229,6 @@ function isNavActive(path: string) {
|
|||||||
<van-tag v-if="listing.rank_level" plain size="medium">{{
|
<van-tag v-if="listing.rank_level" plain size="medium">{{
|
||||||
listing.rank_level
|
listing.rank_level
|
||||||
}}</van-tag>
|
}}</van-tag>
|
||||||
<van-tag
|
|
||||||
v-if="getDailyLoss(listing)"
|
|
||||||
color="#fff7ed"
|
|
||||||
text-color="#ea580c"
|
|
||||||
size="medium"
|
|
||||||
>
|
|
||||||
损耗 {{ getDailyLoss(listing) }}
|
|
||||||
</van-tag>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 class="detail-title">{{ getListingTitle(listing) }}</h2>
|
<h2 class="detail-title">{{ getListingTitle(listing) }}</h2>
|
||||||
<p class="detail-desc">{{ getListingSubtitle(listing) }}</p>
|
<p class="detail-desc">{{ getListingSubtitle(listing) }}</p>
|
||||||
@@ -271,7 +277,12 @@ function isNavActive(path: string) {
|
|||||||
>
|
>
|
||||||
<span>{{ resource.label }}</span>
|
<span>{{ resource.label }}</span>
|
||||||
<strong>{{ resource.quantity }}</strong>
|
<strong>{{ resource.quantity }}</strong>
|
||||||
<em>{{ resource.mode }}</em>
|
<em>
|
||||||
|
<b>{{ resource.mode || "--" }}</b>
|
||||||
|
<small v-if="resource.amount > 0">¥{{ resource.amount }}</small>
|
||||||
|
<small v-else-if="resource.mode === '收费'">{{ resource.price || "¥0" }}</small>
|
||||||
|
<small v-else>无额外收费</small>
|
||||||
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,6 +323,10 @@ function isNavActive(path: string) {
|
|||||||
<span class="price-label">价格</span>
|
<span class="price-label">价格</span>
|
||||||
<span class="price-amount">¥{{ orderTotal }}</span>
|
<span class="price-amount">¥{{ orderTotal }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="order-price-detail">
|
||||||
|
<span>租金 ¥{{ orderPriceBreakdown.rent }}</span>
|
||||||
|
<span>额外 ¥{{ orderPriceBreakdown.consumable }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<van-button
|
<van-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -580,13 +595,28 @@ function isNavActive(path: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.resource-pill em {
|
.resource-pill em {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
color: #ff7900;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-pill em b {
|
||||||
|
color: #ff7900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-pill em small {
|
||||||
|
min-width: 0;
|
||||||
|
color: #17233d;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.skin-detail-group + .skin-detail-group {
|
.skin-detail-group + .skin-detail-group {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
@@ -669,6 +699,16 @@ function isNavActive(path: string) {
|
|||||||
color: #ff5f00;
|
color: #ff5f00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-price-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 8px;
|
||||||
|
color: #8a5a12;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
.order-btn {
|
.order-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
formatRatio,
|
formatRatio,
|
||||||
getCoinWan,
|
getCoinWan,
|
||||||
getDailyLoss,
|
getDailyLoss,
|
||||||
|
getListingConsumablePrice,
|
||||||
getListingChips,
|
getListingChips,
|
||||||
getListingDisplayPrice,
|
getListingDisplayPrice,
|
||||||
|
getListingRentPrice,
|
||||||
getListingResources,
|
getListingResources,
|
||||||
getListingSubtitle,
|
getListingSubtitle,
|
||||||
getListingTitle,
|
getListingTitle,
|
||||||
@@ -46,6 +48,21 @@ const orderTotal = computed(() => {
|
|||||||
return `${Math.round(getListingDisplayPrice(listing.value))}`;
|
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(() => {
|
const coverURL = computed(() => {
|
||||||
if (!listing.value) return "";
|
if (!listing.value) return "";
|
||||||
return listing.value.cover_url || listing.value.screenshot_urls?.[0] || "";
|
return listing.value.cover_url || listing.value.screenshot_urls?.[0] || "";
|
||||||
@@ -53,12 +70,12 @@ const coverURL = computed(() => {
|
|||||||
|
|
||||||
const detailMetrics = computed(() => {
|
const detailMetrics = computed(() => {
|
||||||
if (!listing.value) return [];
|
if (!listing.value) return [];
|
||||||
const totalAssetWan = readAssetNumber(listing.value, "total_asset_wan");
|
const dailyLoss = getDailyLoss(listing.value);
|
||||||
return [
|
return [
|
||||||
{ label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" },
|
{ label: "纯币", value: formatHafCoinM(getCoinWan(listing.value)), tone: "coin" },
|
||||||
{
|
{
|
||||||
label: "总资产",
|
label: "日损耗",
|
||||||
value: totalAssetWan > 0 ? formatHafCoinM(totalAssetWan) : "--",
|
value: dailyLoss ? `${dailyLoss}/天` : "--",
|
||||||
tone: "coin",
|
tone: "coin",
|
||||||
},
|
},
|
||||||
{ label: "价格", value: `¥${orderTotal.value}`, tone: "price" },
|
{ label: "价格", value: `¥${orderTotal.value}`, tone: "price" },
|
||||||
@@ -167,7 +184,6 @@ function listingPrice(item: Listing) {
|
|||||||
<span>{{ getServerRegion(listing) }}</span>
|
<span>{{ getServerRegion(listing) }}</span>
|
||||||
<span v-if="getLoginMethod(listing)">{{ getLoginMethod(listing) }}</span>
|
<span v-if="getLoginMethod(listing)">{{ getLoginMethod(listing) }}</span>
|
||||||
<span v-if="listing.rank_level">{{ listing.rank_level }}</span>
|
<span v-if="listing.rank_level">{{ listing.rank_level }}</span>
|
||||||
<span v-if="getDailyLoss(listing)">损耗 {{ getDailyLoss(listing) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ getListingTitle(listing) }}</h1>
|
<h1>{{ getListingTitle(listing) }}</h1>
|
||||||
<p>{{ getListingSubtitle(listing) }}</p>
|
<p>{{ getListingSubtitle(listing) }}</p>
|
||||||
@@ -209,7 +225,12 @@ function listingPrice(item: Listing) {
|
|||||||
<div v-for="resource in getListingResources(listing)" :key="resource.key" class="detail-resource-card">
|
<div v-for="resource in getListingResources(listing)" :key="resource.key" class="detail-resource-card">
|
||||||
<span>{{ resource.label }}</span>
|
<span>{{ resource.label }}</span>
|
||||||
<strong>{{ resource.quantity }}</strong>
|
<strong>{{ resource.quantity }}</strong>
|
||||||
<em>{{ resource.mode }}</em>
|
<em>
|
||||||
|
<b>{{ resource.mode || "--" }}</b>
|
||||||
|
<small v-if="resource.amount > 0">¥{{ resource.amount }}</small>
|
||||||
|
<small v-else-if="resource.mode === '收费'">{{ resource.price || "¥0" }}</small>
|
||||||
|
<small v-else>无额外收费</small>
|
||||||
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -256,9 +277,21 @@ function listingPrice(item: Listing) {
|
|||||||
<p class="order-safe-text">平台托管订单与押金,按平台交接流程完成账号使用。</p>
|
<p class="order-safe-text">平台托管订单与押金,按平台交接流程完成账号使用。</p>
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<div class="order-total-box">
|
<div class="order-total-box">
|
||||||
<span>价格</span>
|
<div class="order-total-head">
|
||||||
<strong>¥{{ listingPrice(listing) }}</strong>
|
<span>租赁价格</span>
|
||||||
<em>押金 ¥{{ listing.deposit_amount }}</em>
|
<strong>¥{{ listingPrice(listing) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="order-price-breakdown">
|
||||||
|
<div>
|
||||||
|
<span>基础租金</span>
|
||||||
|
<strong>¥{{ orderPriceBreakdown.rent }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>额外物品</span>
|
||||||
|
<strong>¥{{ orderPriceBreakdown.consumable }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<em>押金另付 ¥{{ listing.deposit_amount }}</em>
|
||||||
</div>
|
</div>
|
||||||
<dl class="order-check-list">
|
<dl class="order-check-list">
|
||||||
<div>
|
<div>
|
||||||
@@ -520,13 +553,25 @@ function listingPrice(item: Listing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-resource-card em {
|
.detail-resource-card em {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
color: #ff6a00;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 900;
|
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 {
|
.skin-groups {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -580,6 +625,62 @@ function listingPrice(item: Listing) {
|
|||||||
margin-bottom: 14px;
|
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 {
|
.order-check-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user