优化首页账号卡片展示
This commit is contained in:
@@ -4,21 +4,18 @@ import { RouterLink } from 'vue-router'
|
||||
import type { Listing } from '@/features/listings'
|
||||
import { formatCent, formatMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
formatHafCoinM,
|
||||
getCoinWan,
|
||||
formatEstimatedRentalDuration,
|
||||
getDailyLoss,
|
||||
getListingDisplayPrice,
|
||||
formatListingCode,
|
||||
getListingChips,
|
||||
getListingResources,
|
||||
getListingTitle,
|
||||
getLoginMethod,
|
||||
getOnlineTimeText,
|
||||
getServerRegion,
|
||||
getSkinNames,
|
||||
hasAcceleratedSaleRatio,
|
||||
hasGiftResources,
|
||||
readAssetNumber,
|
||||
readAssetString,
|
||||
getRatioValue,
|
||||
type ListingDisplayResource,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
@@ -27,53 +24,48 @@ interface Props {
|
||||
listing: Listing
|
||||
}
|
||||
|
||||
interface ListingCardStat {
|
||||
label: string
|
||||
value: string
|
||||
tone?: 'accent' | 'highlight'
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
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 chipItems = computed(() => getListingChips(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))
|
||||
}
|
||||
const rentalDuration = computed(() => formatEstimatedRentalDuration(props.listing))
|
||||
const visibleSkinNames = computed(() => skinNames.value.slice(0, 4))
|
||||
const skinOverflowCount = computed(() =>
|
||||
Math.max(0, skinNames.value.length - visibleSkinNames.value.length)
|
||||
)
|
||||
const accessTags = computed(() =>
|
||||
[getServerRegion(props.listing), getLoginMethod(props.listing)]
|
||||
.filter(Boolean)
|
||||
.map(item => accessBadgeMeta(item))
|
||||
)
|
||||
const metaBadges = computed(() =>
|
||||
[
|
||||
{ label: formatListingCode(props.listing), tone: 'code' },
|
||||
dailyLoss.value ? { label: `消耗 ${dailyLoss.value}/天`, tone: 'muted' } : null,
|
||||
rentalDuration.value !== '--' ? { label: `租期 ${rentalDuration.value}`, tone: 'muted' } : null,
|
||||
getRatioValue(props.listing) > 0
|
||||
? { label: `1元=${getRatioValue(props.listing).toFixed(0)}w哈夫币`, tone: 'ratio' }
|
||||
: null,
|
||||
].filter((item): item is { label: string; tone: string } => Boolean(item))
|
||||
)
|
||||
|
||||
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 accessBadgeMeta(value: string) {
|
||||
const text = value.trim()
|
||||
if (text.includes('微信')) return { label: text, icon: 'wechat', tone: 'wechat' }
|
||||
if (text.toUpperCase().includes('QQ')) return { label: text, icon: 'qq', tone: 'qq' }
|
||||
if (text.includes('扫码')) return { label: text, icon: 'scan', tone: 'scan' }
|
||||
if (text.includes('账') || text.includes('密码'))
|
||||
return { label: text, icon: 'lock', tone: 'password' }
|
||||
if (text.includes('Steam')) return { label: text, icon: 'desktop-o', tone: 'steam' }
|
||||
return { label: text, icon: 'bookmark-o', tone: 'default' }
|
||||
}
|
||||
|
||||
function formatStatNumber(value: number) {
|
||||
@@ -100,29 +92,35 @@ function formatStatNumber(value: number) {
|
||||
</div>
|
||||
|
||||
<div class="card-details">
|
||||
<div class="card-title-row">
|
||||
<h3>{{ getListingTitle(listing) }}</h3>
|
||||
<div class="card-title-meta">
|
||||
<span class="listing-no">编号 {{ formatListingCode(listing) }}</span>
|
||||
<span v-if="dailyLoss" class="daily-loss">日耗 {{ dailyLoss }}</span>
|
||||
</div>
|
||||
<h3>{{ getListingTitle(listing) }}</h3>
|
||||
|
||||
<div class="meta-flow">
|
||||
<span v-for="badge in metaBadges" :key="badge.label" :class="`tone-${badge.tone}`">
|
||||
{{ badge.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-panel">
|
||||
<div class="panel-title">基础资料</div>
|
||||
<div class="info-grid">
|
||||
<div v-for="stat in statItems" :key="stat.label" class="info-item">
|
||||
<label>{{ stat.label }}</label>
|
||||
<strong v-if="stat.tone === 'accent'">{{ stat.value }}</strong>
|
||||
<span v-else :class="{ highlight: stat.tone === 'highlight' }" :title="stat.title">
|
||||
{{ stat.value }}<em v-if="stat.label === '持有皮肤' && skinNames.length > 2">...</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="access-flow">
|
||||
<span v-for="tag in accessTags" :key="tag.label" :class="`tone-${tag.tone}`">
|
||||
<van-icon :name="tag.icon" :size="16" />
|
||||
{{ tag.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="resourceItems.length" class="resource-panel">
|
||||
<div class="panel-title">额外物资</div>
|
||||
<div class="chip-flow">
|
||||
<span v-for="chip in chipItems" :key="chip.label" class="info-chip">
|
||||
{{ chip.label }}:{{ chip.value }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="skinNames.length" class="skin-line" :title="skinNames.join('、')">
|
||||
<span class="line-title">皮肤</span>
|
||||
<span class="skin-list">{{ visibleSkinNames.join(' / ') }}</span>
|
||||
<span v-if="skinOverflowCount" class="skin-count">+{{ skinOverflowCount }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="resourceItems.length" class="resource-line">
|
||||
<span class="line-title">额外物资</span>
|
||||
<div class="resource-chips">
|
||||
<span v-for="resource in resourceItems" :key="resource.key" class="resource-chip">
|
||||
<em>{{ resource.label }}</em>
|
||||
@@ -134,17 +132,15 @@ function formatStatNumber(value: number) {
|
||||
</div>
|
||||
|
||||
<div class="card-price">
|
||||
<div class="price-item total">
|
||||
<div class="price-main">
|
||||
<small>总租金</small>
|
||||
<strong>¥{{ formatMoney(getListingDisplayPrice(listing)) }}</strong>
|
||||
</div>
|
||||
<div class="price-item deposit">
|
||||
<div class="price-deposit">
|
||||
<small>押金</small>
|
||||
<span>¥{{ formatCent(listing.deposit_amount_cent) }}</span>
|
||||
</div>
|
||||
<div class="price-action">
|
||||
<button class="rent-btn">立即租用</button>
|
||||
</div>
|
||||
<button class="rent-btn">立即租用</button>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
@@ -152,31 +148,35 @@ function formatStatNumber(value: number) {
|
||||
<style scoped>
|
||||
.listing-card-v2 {
|
||||
display: grid;
|
||||
grid-template-columns: 180px minmax(0, 1fr) 160px;
|
||||
gap: 24px;
|
||||
padding: 20px;
|
||||
border: 1px solid #eef1f5;
|
||||
grid-template-columns: 190px minmax(0, 1fr) 168px;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
padding: 16px;
|
||||
border: 1px solid #e7edf6;
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(23, 35, 61, 0.03);
|
||||
box-shadow: 0 8px 22px rgba(23, 35, 61, 0.04);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
.listing-card-v2:hover {
|
||||
border-color: #ff6a00;
|
||||
box-shadow: 0 8px 24px rgba(255, 106, 0, 0.12);
|
||||
box-shadow: 0 14px 34px rgba(255, 106, 0, 0.13);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f1f5f9;
|
||||
background: #f3f6fb;
|
||||
}
|
||||
|
||||
.card-cover img {
|
||||
@@ -197,8 +197,8 @@ function formatStatNumber(value: number) {
|
||||
|
||||
.cover-badges {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -224,159 +224,172 @@ function formatStatNumber(value: number) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 4px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 8px 10px;
|
||||
background: rgba(33, 28, 24, 0.76);
|
||||
color: #fbbf24;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-title-row h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-title-meta {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.listing-no {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 6px;
|
||||
background: #eff6ff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.daily-loss {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 10px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.info-panel,
|
||||
.resource-panel {
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.resource-panel {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.resource-panel .panel-title {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
|
||||
gap: 8px 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.info-item span,
|
||||
.info-item strong {
|
||||
display: block;
|
||||
.card-details h3 {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
line-height: 1.32;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-item strong {
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.info-item .highlight {
|
||||
color: #10b981;
|
||||
font-weight: 700;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.meta-flow,
|
||||
.access-flow,
|
||||
.chip-flow,
|
||||
.resource-chips {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meta-flow span,
|
||||
.access-flow span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 28px;
|
||||
padding: 0 11px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.meta-flow .tone-code {
|
||||
border: 1px solid #dbeafe;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.meta-flow .tone-muted {
|
||||
background: #eef2f7;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.meta-flow .tone-ratio {
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.access-flow .tone-qq,
|
||||
.access-flow .tone-wechat {
|
||||
background: #eaf3ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.access-flow .tone-scan {
|
||||
background: #fff7ed;
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.access-flow .tone-steam {
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.access-flow .tone-password,
|
||||
.access-flow .tone-default {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.info-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 280px;
|
||||
min-height: 28px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid #fed7aa;
|
||||
border-radius: 999px;
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skin-line,
|
||||
.resource-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
margin-top: 2px;
|
||||
padding: 9px 12px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.skin-line {
|
||||
overflow: hidden;
|
||||
border: 1px solid #edf2f7;
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
.line-title {
|
||||
flex: 0 0 auto;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.skin-list {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skin-count {
|
||||
flex: 0 0 auto;
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.resource-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
max-width: 100%;
|
||||
padding: 6px 10px;
|
||||
min-height: 28px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid #fed7aa;
|
||||
border-radius: 999px;
|
||||
background: #fff7ed;
|
||||
color: #9a3412;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.resource-chip em {
|
||||
@@ -400,52 +413,85 @@ function formatStatNumber(value: number) {
|
||||
}
|
||||
|
||||
.card-price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
padding: 14px 0 14px 18px;
|
||||
border-left: 1px solid #edf2f7;
|
||||
}
|
||||
|
||||
.price-item {
|
||||
.price-main,
|
||||
.price-deposit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.price-item small {
|
||||
font-size: 11px;
|
||||
.price-main small,
|
||||
.price-deposit small {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-item.total strong {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.price-item.deposit span {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
.price-main strong {
|
||||
color: #ff5f00;
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.price-deposit span {
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.rent-btn {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
min-height: 48px;
|
||||
padding: 0 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #ff6a00 0%, #ff8533 100%);
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.rent-btn:hover {
|
||||
background: linear-gradient(135deg, #ff7a1a 0%, #ff9544 100%);
|
||||
transform: scale(1.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.listing-card-v2 {
|
||||
grid-template-columns: 168px minmax(0, 1fr) 156px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card-details h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.meta-flow span,
|
||||
.access-flow span,
|
||||
.info-chip,
|
||||
.resource-chip {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.skin-list,
|
||||
.line-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.price-main strong {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user