feat(mobile): 全面重构移动端首页与详情页布局并统一3*3顶级保险高光视觉

This commit is contained in:
yml2213
2026-09-06 19:49:33 +08:00
parent 5b072e359b
commit 1c922b239c
8 changed files with 1712 additions and 710 deletions
@@ -53,6 +53,12 @@ import {
assetRegions,
hasAcceleratedSaleRatio,
hasGiftResources,
isTopTierInsurance,
getCoinWan,
formatHafCoinM,
readAssetString,
getListingSkinGroups,
getListingResources,
} from '@/shared/utils/listingDisplay'
import { buildMobileHomeListSignature, useMobileHomeCacheStore } from '@/stores/mobileHomeCache'
import { useSessionStore } from '@/stores/session'
@@ -775,16 +781,41 @@ function getMobileRatioText(item: Listing) {
return `1元=${formatted}w哈夫币`
}
function chipTone(label: string) {
if (label.includes('哈夫币')) return 'coin'
if (label.includes('保险')) return 'insurance'
if (label.includes('体力')) return 'stamina'
if (label.includes('负重')) return 'load'
if (label.includes('段位')) return 'rank'
if (label.includes('AWM')) return 'weapon'
if (label.includes('总资产')) return 'asset'
if (label.includes('方便')) return 'time'
return 'default'
function getItemHafCoin(item: Listing) {
return formatHafCoinM(getCoinWan(item))
}
function getItemInsurance(item: Listing) {
return readAssetString(item, 'season_insurance') || '--'
}
function getItemRatioText(item: Listing) {
const val = getRatioValue(item)
if (val <= 0) return null
return `1元=${val.toFixed(0)}w`
}
function getItemSecondaryChips(item: Listing) {
return getListingChips(item).filter(
c => c.label !== '哈夫币' && c.label !== '保险格数' && c.label !== 'AWM'
)
}
function getItemSkinPreview(item: Listing) {
const groups = getListingSkinGroups(item)
const list: Array<{ name: string; type: string }> = []
for (const group of groups) {
for (const opt of group.options) {
list.push({ name: opt, type: group.key })
if (list.length >= 3) return list
}
}
return list
}
function getItemResourcePreview(item: Listing) {
const resources = getListingResources(item)
return resources.slice(0, 2)
}
syncMobileHomeQuery()
@@ -979,7 +1010,7 @@ syncMobileHomeQuery()
<van-button size="small" type="primary" @click="clearFilters"> 重置条件 </van-button>
</van-empty>
<!-- 列表卡片:全宽上下布局 -->
<!-- 列表卡片:现代结构化卡片(对齐PC首页体验) -->
<div class="mobile-list">
<RouterLink
v-for="item in displayListings"
@@ -987,81 +1018,141 @@ syncMobileHomeQuery()
class="mobile-card"
:to="`/m/listings/${item.id}`"
>
<div class="card-cover">
<img
v-if="item.cover_url"
:src="item.cover_url"
:alt="getListingTitle(item)"
loading="lazy"
decoding="async"
/>
<span v-else>图</span>
<div
v-if="hasGiftResources(item) || hasAcceleratedSaleRatio(item)"
class="card-cover-labels"
>
<em v-if="hasGiftResources(item)">有赠送</em>
<em v-if="hasAcceleratedSaleRatio(item)">特惠</em>
</div>
</div>
<div class="card-main">
<div class="card-title-row">
<h2>{{ getListingTitle(item) }}</h2>
</div>
<div class="card-meta-row">
<div class="mobile-listing-no">{{ formatListingCode(item) }}</div>
<div
v-if="getDailyLoss(item) || formatEstimatedRentalDuration(item) !== '--'"
class="rental-meta-badge"
<!-- 顶部精简元信息栏 -->
<div class="mobile-card-header">
<div class="header-tags-left">
<span class="code-badge">{{ formatListingCode(item) }}</span>
<span
v-if="getServerRegion(item)"
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getServerRegion(item)).tone}`"
>
<span v-if="getDailyLoss(item)">消耗 {{ getDailyLoss(item) }}/天</span>
<span
v-if="getDailyLoss(item) && formatEstimatedRentalDuration(item) !== '--'"
class="meta-separator"
>
·
</span>
<span v-if="formatEstimatedRentalDuration(item) !== '--'">
租期 {{ formatEstimatedRentalDuration(item) }}
</span>
<van-icon :name="getAccessBadgeMeta(getServerRegion(item)).icon" :size="11" />
<span>{{ getServerRegion(item) }}</span>
</span>
<span
v-if="getLoginMethod(item)"
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getLoginMethod(item)).tone}`"
>
<van-icon :name="getAccessBadgeMeta(getLoginMethod(item)).icon" :size="11" />
<span>{{ getLoginMethod(item) }}</span>
</span>
</div>
<div class="header-tags-right">
<span v-if="formatEstimatedRentalDuration(item) !== '--'" class="aux-pill rental">
可租 {{ formatEstimatedRentalDuration(item) }}
</span>
<span v-if="getDailyLoss(item)" class="aux-pill loss">
耗 {{ getDailyLoss(item) }}/天
</span>
</div>
</div>
<!-- 中部:封面 + 核心资产与高光展台 -->
<div class="mobile-card-body">
<!-- 左侧封面 -->
<div class="mobile-card-media">
<img
v-if="item.cover_url || item.screenshot_urls?.[0]"
:src="item.cover_url || item.screenshot_urls?.[0]"
:alt="formatListingCode(item)"
loading="lazy"
decoding="async"
/>
<div v-else class="media-empty">HFB</div>
<div
v-if="hasGiftResources(item) || hasAcceleratedSaleRatio(item)"
class="media-badges"
>
<span v-if="hasAcceleratedSaleRatio(item)" class="badge sale">特惠</span>
<span v-if="hasGiftResources(item)" class="badge gift">有赠送</span>
</div>
<div v-if="getRatioValue(item) > 0" class="media-ratio">
比例 1:{{ getRatioValue(item).toFixed(1) }}
</div>
</div>
<div class="card-action-row">
<p v-if="getMobileRatioText(item)" class="card-subtitle">
{{ getMobileRatioText(item) }}
</p>
<div class="card-badges-row">
<span
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getServerRegion(item)).tone}`"
<!-- 右侧信息流 -->
<div class="mobile-card-info">
<!-- 核心资产三大微卡 (对齐PC,3*3橙底高光) -->
<div class="card-core-assets">
<div class="core-asset-card coin-card">
<span class="c-label">哈夫币</span>
<strong class="c-val orange">{{ getItemHafCoin(item) }}</strong>
</div>
<div
class="core-asset-card slot-card"
:class="{ 'top-insurance': isTopTierInsurance(getItemInsurance(item)) }"
>
<van-icon :name="getAccessBadgeMeta(getServerRegion(item)).icon" :size="13" />
{{ getServerRegion(item) }}
<span class="c-label">保险</span>
<strong
class="c-val"
:class="isTopTierInsurance(getItemInsurance(item)) ? 'orange' : 'slate'"
>{{ getItemInsurance(item) }}</strong>
</div>
<div v-if="getItemRatioText(item)" class="core-asset-card ratio-card">
<span class="c-label">币比</span>
<strong class="c-val amber">{{ getItemRatioText(item) }}</strong>
</div>
</div>
<!-- 高光皮肤与仓库物资微胶囊 -->
<div
v-if="getItemSkinPreview(item).length || getItemResourcePreview(item).length"
class="card-highlights"
>
<span
v-for="skin in getItemSkinPreview(item)"
:key="skin.name"
class="mobile-skin-pill"
:class="`skin-type-${skin.type}`"
>
<i class="quality-dot" />
<span>{{ skin.name }}</span>
</span>
<span
v-if="getLoginMethod(item)"
class="server-badge"
:class="`tone-${getAccessBadgeMeta(getLoginMethod(item)).tone}`"
v-for="res in getItemResourcePreview(item)"
:key="res.key"
class="mobile-res-pill"
>
<van-icon :name="getAccessBadgeMeta(getLoginMethod(item)).icon" :size="13" />
{{ getLoginMethod(item) }}
<span class="r-label">{{ res.label }}</span>
<strong class="r-qty">{{ res.quantity }}</strong>
<em v-if="res.mode === '赠送'" class="r-gift">送</em>
</span>
</div>
</div>
<div class="card-footer">
<div class="price-col">
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
<span class="rent-sub">押金 ¥{{ formatCent(item.deposit_amount_cent) }}</span>
<!-- 价格与立即租用行动区 -->
<div class="card-price-action">
<div class="price-wrap">
<div class="price-main">
<small>¥</small>
<strong>{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
</div>
<span class="deposit-text">
押金 {{ item.deposit_amount_cent > 0 ? `¥${formatCent(item.deposit_amount_cent)}` : '免押' }}
</span>
</div>
<div class="rent-cta-btn">
<span>去租用</span>
<van-icon name="arrow" :size="10" />
</div>
</div>
</div>
</div>
<div class="card-chip-row">
<!-- 底部:过滤后的次要规格流 (无多余重复) -->
<div v-if="getItemSecondaryChips(item).length" class="card-specs-row">
<span
v-for="chip in getListingChips(item)"
:key="`${item.id}-${chip.label}`"
:class="`tone-${chipTone(chip.label)}`"
v-for="chip in getItemSecondaryChips(item).slice(0, 5)"
:key="chip.label"
class="spec-chip"
>
{{ chip.label }}:{{ chip.value }}
<span class="k">{{ chip.label }}</span>
<strong class="v">{{ chip.value }}</strong>
</span>
</div>
</RouterLink>