优化列表资产展示逻辑
This commit is contained in:
@@ -88,7 +88,7 @@ const screenshotColumns = [
|
|||||||
{
|
{
|
||||||
label: '租期',
|
label: '租期',
|
||||||
width: 92,
|
width: 92,
|
||||||
read: (row: Listing) => `${formatEstimatedRentalDuration(row)}\n日耗 ${getDailyLoss(row)}`,
|
read: (row: Listing) => rentalPeriodText(row),
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
@@ -376,7 +376,15 @@ function rentAndDepositText(row: Listing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function estimateTitle(row: Listing) {
|
function estimateTitle(row: Listing) {
|
||||||
return `按哈夫币 ${listingCoinM(row)}、日耗 ${getDailyLoss(row)} 估算`
|
const dailyLoss = getDailyLoss(row)
|
||||||
|
return dailyLoss ? `按哈夫币 ${listingCoinM(row)}、日耗 ${dailyLoss} 估算` : '未配置日耗'
|
||||||
|
}
|
||||||
|
|
||||||
|
function rentalPeriodText(row: Listing) {
|
||||||
|
const dailyLoss = getDailyLoss(row)
|
||||||
|
return [formatEstimatedRentalDuration(row), dailyLoss ? `日耗 ${dailyLoss}` : '']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatQuantity(value: number) {
|
function formatQuantity(value: number) {
|
||||||
@@ -547,7 +555,7 @@ function formatQuantity(value: number) {
|
|||||||
<el-table-column label="租期" width="82">
|
<el-table-column label="租期" width="82">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :title="estimateTitle(row)">{{ formatEstimatedRentalDuration(row) }}</span>
|
<span :title="estimateTitle(row)">{{ formatEstimatedRentalDuration(row) }}</span>
|
||||||
<span class="table-subtext">日耗 {{ getDailyLoss(row) }}</span>
|
<span v-if="getDailyLoss(row)" class="table-subtext">日耗 {{ getDailyLoss(row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="详情" width="60">
|
<el-table-column label="详情" width="60">
|
||||||
|
|||||||
@@ -5,28 +5,78 @@ import type { Listing } from '@/features/listings'
|
|||||||
import {
|
import {
|
||||||
formatHafCoinM,
|
formatHafCoinM,
|
||||||
getCoinWan,
|
getCoinWan,
|
||||||
|
getDailyLoss,
|
||||||
getListingDisplayPrice,
|
getListingDisplayPrice,
|
||||||
|
getListingResources,
|
||||||
getListingTitle,
|
getListingTitle,
|
||||||
getLoginMethod,
|
getLoginMethod,
|
||||||
|
getOnlineTimeText,
|
||||||
getServerRegion,
|
getServerRegion,
|
||||||
|
getSkinNames,
|
||||||
hasAcceleratedSaleRatio,
|
hasAcceleratedSaleRatio,
|
||||||
hasGiftResources,
|
hasGiftResources,
|
||||||
readAssetNumber,
|
readAssetNumber,
|
||||||
readAssetString,
|
readAssetString,
|
||||||
getResourceQuantity,
|
|
||||||
getOnlineTimeText,
|
|
||||||
getSkinNames,
|
|
||||||
getRatioValue,
|
getRatioValue,
|
||||||
getDailyLoss,
|
type ListingDisplayResource,
|
||||||
} from '@/utils/listingDisplay'
|
} from '@/utils/listingDisplay'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
listing: Listing
|
listing: Listing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ListingCardStat {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
tone?: 'accent' | 'highlight'
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const coverURL = computed(() => props.listing.cover_url || props.listing.screenshot_urls?.[0] || '')
|
const coverURL = computed(() => props.listing.cover_url || props.listing.screenshot_urls?.[0] || '')
|
||||||
|
const dailyLoss = computed(() => getDailyLoss(props.listing))
|
||||||
|
const skinNames = computed(() => getSkinNames(props.listing))
|
||||||
|
const statItems = computed(() => buildStatItems(props.listing))
|
||||||
|
const resourceItems = computed(() => getListingResources(props.listing))
|
||||||
|
|
||||||
|
function buildStatItems(listing: Listing) {
|
||||||
|
const onlineTime = getOnlineTimeText(listing)
|
||||||
|
const secretKD = readAssetNumber(listing, 'secret_kd')
|
||||||
|
const skinText = skinNames.value.slice(0, 2).join('、')
|
||||||
|
const stats: ListingCardStat[] = [
|
||||||
|
{ label: '哈夫币', value: formatHafCoinM(getCoinWan(listing)), tone: 'accent' },
|
||||||
|
{ label: '保险格数', value: readAssetString(listing, 'season_insurance') },
|
||||||
|
{ label: '体力/负重', value: formatLevelPair(listing) },
|
||||||
|
{ label: '绝密KD', value: secretKD > 0 ? formatStatNumber(secretKD) : '' },
|
||||||
|
{ label: '游戏段位', value: listing.rank_level },
|
||||||
|
{ label: '所属区服', value: getServerRegion(listing) },
|
||||||
|
{ label: '上号方式', value: getLoginMethod(listing) },
|
||||||
|
{ label: '方便上号', value: onlineTime },
|
||||||
|
{
|
||||||
|
label: '持有皮肤',
|
||||||
|
value: skinText,
|
||||||
|
title: skinNames.value.join('、'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return stats.filter(stat => Boolean(stat.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatResourceQuantity(resource: ListingDisplayResource) {
|
||||||
|
const suffix = resource.key === 'awmAmmo' ? '发' : ''
|
||||||
|
return `${formatStatNumber(resource.quantity)}${suffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLevelPair(listing: Listing) {
|
||||||
|
const staminaLevel = readAssetString(listing, 'stamina_level')
|
||||||
|
const loadLevel = readAssetString(listing, 'load_level')
|
||||||
|
return [staminaLevel, loadLevel].filter(Boolean).join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStatNumber(value: number) {
|
||||||
|
return Number.isInteger(value) ? String(value) : value.toFixed(1)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -50,83 +100,30 @@ const coverURL = computed(() => props.listing.cover_url || props.listing.screens
|
|||||||
<div class="card-details">
|
<div class="card-details">
|
||||||
<div class="card-title-row">
|
<div class="card-title-row">
|
||||||
<h3>{{ getListingTitle(listing) }}</h3>
|
<h3>{{ getListingTitle(listing) }}</h3>
|
||||||
<span class="daily-loss">日耗 {{ getDailyLoss(listing) }}</span>
|
<span v-if="dailyLoss" class="daily-loss">日耗 {{ dailyLoss }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-grid">
|
<div class="info-panel">
|
||||||
<div class="stats-col">
|
<div class="panel-title">基础资料</div>
|
||||||
<div class="stat-row">
|
<div class="info-grid">
|
||||||
<label>哈夫币</label>
|
<div v-for="stat in statItems" :key="stat.label" class="info-item">
|
||||||
<strong>{{ formatHafCoinM(getCoinWan(listing)) }}</strong>
|
<label>{{ stat.label }}</label>
|
||||||
</div>
|
<strong v-if="stat.tone === 'accent'">{{ stat.value }}</strong>
|
||||||
<div class="stat-row">
|
<span v-else :class="{ highlight: stat.tone === 'highlight' }" :title="stat.title">
|
||||||
<label>保险格数</label>
|
{{ stat.value }}<em v-if="stat.label === '持有皮肤' && skinNames.length > 2">...</em>
|
||||||
<span>{{ readAssetString(listing, 'season_insurance') || '--' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-row">
|
|
||||||
<label>体力/负重</label>
|
|
||||||
<span
|
|
||||||
>{{ readAssetString(listing, 'stamina_level') }}/{{
|
|
||||||
readAssetString(listing, 'load_level')
|
|
||||||
}}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats-col">
|
|
||||||
<div class="stat-row">
|
|
||||||
<label>AWM子弹</label>
|
|
||||||
<span :class="{ highlight: getResourceQuantity(listing, 'awmAmmo') > 0 }">
|
|
||||||
{{ getResourceQuantity(listing, 'awmAmmo') }}发
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-row">
|
|
||||||
<label>六级头甲</label>
|
|
||||||
<span>
|
|
||||||
{{ getResourceQuantity(listing, 'helmet6') }}头 /
|
|
||||||
{{ getResourceQuantity(listing, 'armor6') }}甲
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-row">
|
|
||||||
<label>其他重器</label>
|
|
||||||
<span
|
|
||||||
>巴雷特 {{ getResourceQuantity(listing, 'barrett') }} / 喷子
|
|
||||||
{{ getResourceQuantity(listing, 'shotgun') }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="stats-col">
|
<div v-if="resourceItems.length" class="resource-panel">
|
||||||
<div class="stat-row">
|
<div class="panel-title">额外物资</div>
|
||||||
<label>绝密KD</label>
|
<div class="resource-chips">
|
||||||
<strong>{{ readAssetNumber(listing, 'secret_kd') || '--' }}</strong>
|
<span v-for="resource in resourceItems" :key="resource.key" class="resource-chip">
|
||||||
</div>
|
<em>{{ resource.label }}</em>
|
||||||
<div class="stat-row">
|
<strong>{{ formatResourceQuantity(resource) }}</strong>
|
||||||
<label>游戏段位</label>
|
<small v-if="resource.mode === '赠送'">赠送</small>
|
||||||
<span>{{ listing.rank_level || '未公开' }}</span>
|
</span>
|
||||||
</div>
|
|
||||||
<div class="stat-row">
|
|
||||||
<label>所属区服</label>
|
|
||||||
<span>{{ getServerRegion(listing) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats-col">
|
|
||||||
<div class="stat-row">
|
|
||||||
<label>上号方式</label>
|
|
||||||
<span>{{ getLoginMethod(listing) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-row">
|
|
||||||
<label>方便上号</label>
|
|
||||||
<span class="time-text">{{ getOnlineTimeText(listing) || '全天候' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-row skins-row">
|
|
||||||
<label>持有皮肤</label>
|
|
||||||
<span class="skins-text" :title="getSkinNames(listing).join(', ')">
|
|
||||||
{{ getSkinNames(listing).slice(0, 2).join(', ') || '暂无皮肤' }}
|
|
||||||
<em v-if="getSkinNames(listing).length > 2">...</em>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,14 +230,16 @@ const coverURL = computed(() => props.listing.cover_url || props.listing.screens
|
|||||||
.card-details {
|
.card-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title-row {
|
.card-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title-row h3 {
|
.card-title-row h3 {
|
||||||
@@ -254,6 +253,7 @@ const coverURL = computed(() => props.listing.cover_url || props.listing.screens
|
|||||||
}
|
}
|
||||||
|
|
||||||
.daily-loss {
|
.daily-loss {
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -262,46 +262,121 @@ const coverURL = computed(() => props.listing.cover_url || props.listing.screens
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.info-panel,
|
||||||
display: grid;
|
.resource-panel {
|
||||||
grid-template-columns: repeat(4, 1fr);
|
border-radius: 14px;
|
||||||
gap: 20px;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-col {
|
.info-panel {
|
||||||
|
padding: 10px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row {
|
.panel-title {
|
||||||
display: flex;
|
flex: 0 0 auto;
|
||||||
flex-direction: column;
|
margin-bottom: 8px;
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-row label {
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-weight: 600;
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row span,
|
.resource-panel .panel-title {
|
||||||
.stat-row strong {
|
margin: 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
|
||||||
|
gap: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item span,
|
||||||
|
.info-item strong {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row strong {
|
.info-item strong {
|
||||||
color: #ff6a00;
|
color: #ff6a00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-row .highlight {
|
.info-item .highlight {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-chips {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #9a3412;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip em {
|
||||||
|
overflow: hidden;
|
||||||
|
font-style: normal;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip strong {
|
||||||
|
color: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-chip small {
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #15803d;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.card-price {
|
.card-price {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -337,7 +337,6 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trust-badge,
|
|
||||||
.server-badge,
|
.server-badge,
|
||||||
.card-chip-row span {
|
.card-chip-row span {
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
@@ -347,11 +346,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trust-badge {
|
|
||||||
background: #ecfdf5;
|
|
||||||
color: #047857;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -512,7 +512,6 @@ function uniqueOptions(values: string[]) {
|
|||||||
</div>
|
</div>
|
||||||
<p class="card-subtitle">{{ getListingSubtitle(item) }}</p>
|
<p class="card-subtitle">{{ getListingSubtitle(item) }}</p>
|
||||||
<div class="card-badges-row">
|
<div class="card-badges-row">
|
||||||
<span class="trust-badge">押金秒退</span>
|
|
||||||
<span class="server-badge">{{ getServerRegion(item) }}</span>
|
<span class="server-badge">{{ getServerRegion(item) }}</span>
|
||||||
<span v-if="getLoginMethod(item)" class="server-badge">
|
<span v-if="getLoginMethod(item)" class="server-badge">
|
||||||
{{ getLoginMethod(item) }}
|
{{ getLoginMethod(item) }}
|
||||||
|
|||||||
@@ -693,13 +693,17 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
season_insurance: form.season_insurance,
|
season_insurance: form.season_insurance,
|
||||||
stamina_level: form.stamina_level,
|
stamina_level: form.stamina_level,
|
||||||
load_level: form.load_level,
|
load_level: form.load_level,
|
||||||
resources: pricing.quantityItems.value.map(item => ({
|
resources: pricing.quantityItems.value
|
||||||
key: item.key,
|
.map(item => ({
|
||||||
label: item.label,
|
key: item.key,
|
||||||
price: item.price,
|
label: item.label,
|
||||||
quantity: pricing.isQuantityItemDisabled(item) ? 0 : Number(quantityValues[item.key] || 0),
|
price: item.price,
|
||||||
mode: pricing.isQuantityItemDisabled(item) ? '赠送' : quantityModes[item.key] || '收费',
|
quantity: pricing.isQuantityItemDisabled(item)
|
||||||
})),
|
? 0
|
||||||
|
: Number(quantityValues[item.key] || 0),
|
||||||
|
mode: pricing.isQuantityItemDisabled(item) ? '赠送' : quantityModes[item.key] || '收费',
|
||||||
|
}))
|
||||||
|
.filter(item => item.quantity > 0),
|
||||||
skin_groups: pricing.skinGroups.value.reduce<Record<string, string[]>>((groups, group) => {
|
skin_groups: pricing.skinGroups.value.reduce<Record<string, string[]>>((groups, group) => {
|
||||||
groups[group.key] = group.options.filter(skin => selectedSkins.value.includes(skin))
|
groups[group.key] = group.options.filter(skin => selectedSkins.value.includes(skin))
|
||||||
return groups
|
return groups
|
||||||
|
|||||||
@@ -219,11 +219,7 @@ export function getDailyLoss(item: Listing) {
|
|||||||
export function getDailyLossM(item: Listing) {
|
export function getDailyLossM(item: Listing) {
|
||||||
const configuredLoss = readAssetNumber(item, 'daily_loss_m')
|
const configuredLoss = readAssetNumber(item, 'daily_loss_m')
|
||||||
if (configuredLoss > 0) return configuredLoss
|
if (configuredLoss > 0) return configuredLoss
|
||||||
|
return 0
|
||||||
const coinWan = getCoinWan(item)
|
|
||||||
if (coinWan >= 30000) return 30
|
|
||||||
if (coinWan >= 10000) return 20
|
|
||||||
return 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEstimatedRentalDays(item: Listing) {
|
export function getEstimatedRentalDays(item: Listing) {
|
||||||
|
|||||||
@@ -219,11 +219,7 @@ export function getDailyLoss(item: Listing) {
|
|||||||
export function getDailyLossM(item: Listing) {
|
export function getDailyLossM(item: Listing) {
|
||||||
const configuredLoss = readAssetNumber(item, 'daily_loss_m')
|
const configuredLoss = readAssetNumber(item, 'daily_loss_m')
|
||||||
if (configuredLoss > 0) return configuredLoss
|
if (configuredLoss > 0) return configuredLoss
|
||||||
|
return 0
|
||||||
const coinWan = getCoinWan(item)
|
|
||||||
if (coinWan >= 30000) return 30
|
|
||||||
if (coinWan >= 10000) return 20
|
|
||||||
return 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEstimatedRentalDays(item: Listing) {
|
export function getEstimatedRentalDays(item: Listing) {
|
||||||
|
|||||||
Reference in New Issue
Block a user