style(listing): 全面重构PC商品详情页UI视觉层级与多图验号展台
- 解耦旧版暗黑压图Banner,重构为高清晰卡片抬头与一键复制商品编号 - 新增16:9多维验号图片展台,支持分类角标、全屏灯箱放大与缩略图胶卷导轨 - 对齐首页视觉规范,以四大核心资产微卡(哈夫币/保险/币比/可租损耗)为视觉中心 - 升级账号核心档案12宫格矩阵与高光皮肤品级圆点展台 - 重构右侧粘性常驻结算下单面板,优化暖色价格卡与平台官方交易保障微标
This commit is contained in:
+2
-1
@@ -42,4 +42,5 @@ logs
|
||||
.gocache
|
||||
artifacts/
|
||||
|
||||
frontend/coverage/
|
||||
frontend/coverage/
|
||||
.zcode/
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import { readError } from '@/shared/utils/error'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ShoppingCart, Star, StarFilled, ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { ShoppingCart, Star, StarFilled, ArrowLeft, DocumentCopy } from '@element-plus/icons-vue'
|
||||
import { goBackOrHome } from '@/shared/utils/routerBack'
|
||||
|
||||
import { fetchListing, type Listing } from '@/features/listings/api/listings'
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
getServerRegion,
|
||||
readAssetNumber,
|
||||
readAssetString,
|
||||
getRatioValue,
|
||||
type ListingDisplayResource,
|
||||
} from '@/shared/utils/listingDisplay'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -56,6 +58,8 @@ const virtualAgreementRef = ref<HTMLElement | null>(null)
|
||||
const renterAgreementRef = ref<HTMLElement | null>(null)
|
||||
const { recordListingView, isFavorite, toggleFavorite } = useListingCollections()
|
||||
|
||||
const activeImageIndex = ref(0)
|
||||
|
||||
const canCreateOrderAfterAgreement = computed(
|
||||
() =>
|
||||
virtualAgreementRead.value &&
|
||||
@@ -97,32 +101,57 @@ const orderPriceBreakdown = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const coverURL = computed(() => {
|
||||
// 核心资产微卡计算(同首页设计标准)
|
||||
const hafCoinDisplay = computed(() => {
|
||||
if (!listing.value) return '--'
|
||||
return formatHafCoinM(getCoinWan(listing.value))
|
||||
})
|
||||
|
||||
const insuranceSlot = computed(() => {
|
||||
if (!listing.value) return '--'
|
||||
return readAssetString(listing.value, 'season_insurance') || '--'
|
||||
})
|
||||
|
||||
const ratioValue = computed(() => {
|
||||
if (!listing.value) return 0
|
||||
return getRatioValue(listing.value)
|
||||
})
|
||||
|
||||
const ratioDisplay = computed(() => {
|
||||
if (ratioValue.value <= 0) return null
|
||||
return `1元 = ${ratioValue.value.toFixed(1)}w`
|
||||
})
|
||||
|
||||
const rentalDuration = computed(() => {
|
||||
if (!listing.value) return '--'
|
||||
return formatEstimatedRentalDuration(listing.value)
|
||||
})
|
||||
|
||||
const dailyLoss = computed(() => {
|
||||
if (!listing.value) return ''
|
||||
return listing.value.cover_url || listing.value.screenshot_urls?.[0] || ''
|
||||
return getDailyLoss(listing.value)
|
||||
})
|
||||
|
||||
const detailMetrics = computed(() => {
|
||||
// 平台/区服特性徽标
|
||||
const accessTags = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const dailyLoss = getDailyLoss(listing.value)
|
||||
const secretKD = readAssetNumber(listing.value, 'secret_kd')
|
||||
return [
|
||||
{ label: '纯币', value: formatHafCoinM(getCoinWan(listing.value)), tone: 'coin' },
|
||||
{
|
||||
label: '绝密KD',
|
||||
value: secretKD > 0 ? formatAssetNumber(secretKD) : '--',
|
||||
tone: 'coin',
|
||||
},
|
||||
{
|
||||
label: '日损耗',
|
||||
value: dailyLoss ? `${dailyLoss}/天` : '--',
|
||||
tone: 'coin',
|
||||
},
|
||||
{ label: '价格', value: `¥${orderTotal.value}`, tone: 'price' },
|
||||
{ label: '押金', value: `¥${formatCent(listing.value.deposit_amount_cent)}`, tone: '' },
|
||||
]
|
||||
return [getServerRegion(listing.value), getLoginMethod(listing.value)]
|
||||
.filter(Boolean)
|
||||
.map(item => accessBadgeMeta(item))
|
||||
})
|
||||
|
||||
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' }
|
||||
}
|
||||
|
||||
// 截图多图展台
|
||||
const detailScreenshots = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const groupedScreenshots = readGroupedScreenshots(listing.value)
|
||||
@@ -135,15 +164,20 @@ const detailScreenshots = computed(() => {
|
||||
})
|
||||
const detailScreenshotUrls = computed(() => detailScreenshots.value.map(shot => shot.url))
|
||||
|
||||
const activeScreenshot = computed(() => {
|
||||
if (detailScreenshots.value.length === 0) return null
|
||||
return detailScreenshots.value[activeImageIndex.value] || detailScreenshots.value[0]
|
||||
})
|
||||
|
||||
function readGroupedScreenshots(item: Listing) {
|
||||
const groups = item.asset_summary?.screenshot_groups
|
||||
if (typeof groups !== 'object' || groups === null) return []
|
||||
const slots = [
|
||||
{ key: 'coin', label: '纯币截图' },
|
||||
{ key: 'gameId', label: '游戏ID截图' },
|
||||
{ key: 'totalAsset', label: '总资产截图' },
|
||||
{ key: 'tencentSecurity', label: '腾讯安全中心截图' },
|
||||
{ key: 'skin', label: '皮肤截图' },
|
||||
{ key: 'gameId', label: '游戏ID截图' },
|
||||
{ key: 'totalAsset', label: '总资产截图' },
|
||||
]
|
||||
return slots.flatMap(slot => {
|
||||
const urls = (groups as Record<string, unknown>)[slot.key]
|
||||
@@ -156,30 +190,51 @@ function readGroupedScreenshots(item: Listing) {
|
||||
})
|
||||
}
|
||||
|
||||
// 皮肤清单(带高光品级)
|
||||
const detailSkinGroups = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const groups = listing.value.asset_summary?.skin_groups
|
||||
if (typeof groups !== 'object' || groups === null) return []
|
||||
const titles: Record<string, string> = {
|
||||
melee: '近战皮肤',
|
||||
operator: '干员皮肤',
|
||||
operatorGold: '干员金皮',
|
||||
operatorRed: '干员红皮',
|
||||
weapon: '武器皮肤',
|
||||
other: '其他皮肤',
|
||||
imported: '其他皮肤',
|
||||
const titles: Record<string, { title: string; type: string }> = {
|
||||
melee: { title: '近战皮肤', type: 'melee' },
|
||||
operatorRed: { title: '干员红皮', type: 'operatorRed' },
|
||||
operatorGold: { title: '干员金皮', type: 'operatorGold' },
|
||||
operator: { title: '干员皮肤', type: 'operator' },
|
||||
weapon: { title: '武器皮肤', type: 'weapon' },
|
||||
other: { title: '其他皮肤', type: 'other' },
|
||||
imported: { title: '其他皮肤', type: 'other' },
|
||||
}
|
||||
return Object.entries(groups as Record<string, unknown>)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
title: titles[key] || key,
|
||||
options: Array.isArray(value)
|
||||
? value.filter((skin): skin is string => typeof skin === 'string')
|
||||
: [],
|
||||
}))
|
||||
.map(([key, value]) => {
|
||||
const meta = titles[key] || { title: key, type: 'other' }
|
||||
return {
|
||||
key,
|
||||
type: meta.type,
|
||||
title: meta.title,
|
||||
options: Array.isArray(value)
|
||||
? value.filter((skin): skin is string => typeof skin === 'string')
|
||||
: [],
|
||||
}
|
||||
})
|
||||
.filter(group => group.options.length)
|
||||
})
|
||||
|
||||
const totalSkinCount = computed(() => {
|
||||
return detailSkinGroups.value.reduce((acc, g) => acc + g.options.length, 0)
|
||||
})
|
||||
|
||||
// 规格胶囊(过滤已展示的哈夫币、保险、AWM)
|
||||
const secondaryChips = computed(() => {
|
||||
if (!listing.value) return []
|
||||
return getListingChips(listing.value).filter(
|
||||
chip =>
|
||||
chip.label !== '哈夫币' &&
|
||||
chip.label !== '保险格数' &&
|
||||
chip.label !== 'AWM'
|
||||
)
|
||||
})
|
||||
|
||||
// 账号基础档案 12 宫格
|
||||
const accountRows = computed(() => {
|
||||
if (!listing.value) return []
|
||||
const regions = assetRegions(listing.value)
|
||||
@@ -197,10 +252,22 @@ const accountRows = computed(() => {
|
||||
{ label: '方便上号', value: getOnlineTimeText(listing.value) || '--' },
|
||||
{ label: '预计可租', value: formatEstimatedRentalDuration(listing.value) },
|
||||
{ label: '常用登录地', value: regions.length ? regions.join('、') : '--' },
|
||||
{ label: '封禁记录', value: readAssetString(listing.value, 'ban_record') || '无' },
|
||||
{ label: '封禁记录', value: readAssetString(listing.value, 'ban_record') || '无封禁记录' },
|
||||
]
|
||||
})
|
||||
|
||||
// 仓库物资
|
||||
const resourceItems = computed(() => {
|
||||
if (!listing.value) return []
|
||||
return getListingResources(listing.value)
|
||||
})
|
||||
|
||||
function formatResourceQuantity(resource: ListingDisplayResource) {
|
||||
const suffix = resource.key === 'awmAmmo' ? '发' : ''
|
||||
const val = Number.isInteger(resource.quantity) ? String(resource.quantity) : resource.quantity.toFixed(1)
|
||||
return `${val}${suffix}`
|
||||
}
|
||||
|
||||
async function handleCreateOrder() {
|
||||
if (!listing.value) return
|
||||
if (!session.token) {
|
||||
@@ -279,7 +346,7 @@ async function copyListingCode() {
|
||||
if (!listing.value) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(formatListingCode(listing.value))
|
||||
ElMessage.success('商品编号已复制')
|
||||
ElMessage.success('商品编号已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败')
|
||||
}
|
||||
@@ -300,47 +367,33 @@ function updateAgreementReadState(type: 'virtual' | 'renter') {
|
||||
}
|
||||
}
|
||||
|
||||
// 手动锁定/解锁背景滚动
|
||||
// 锁定/解锁背景滚动
|
||||
watch(agreementVisible, visible => {
|
||||
// 只锁定 .pc-main 容器,不影响 body 和 sticky 导航栏
|
||||
const pcMain = document.querySelector('.pc-main') as HTMLElement
|
||||
if (!pcMain) return
|
||||
|
||||
if (visible) {
|
||||
// 保存当前滚动位置
|
||||
const scrollTop = pcMain.scrollTop || 0
|
||||
|
||||
// 锁定主容器滚动
|
||||
pcMain.style.overflow = 'hidden'
|
||||
pcMain.style.position = 'fixed'
|
||||
pcMain.style.top = `-${scrollTop}px`
|
||||
pcMain.style.left = '0'
|
||||
pcMain.style.right = '0'
|
||||
pcMain.style.width = '100%'
|
||||
|
||||
// 保存滚动位置供恢复使用
|
||||
pcMain.dataset.scrollTop = String(scrollTop)
|
||||
} else {
|
||||
// 恢复主容器滚动
|
||||
const scrollTop = parseInt(pcMain.dataset.scrollTop || '0', 10)
|
||||
|
||||
pcMain.style.overflow = ''
|
||||
pcMain.style.position = ''
|
||||
pcMain.style.top = ''
|
||||
pcMain.style.left = ''
|
||||
pcMain.style.right = ''
|
||||
pcMain.style.width = ''
|
||||
|
||||
// 恢复滚动位置
|
||||
pcMain.scrollTop = scrollTop
|
||||
delete pcMain.dataset.scrollTop
|
||||
}
|
||||
})
|
||||
|
||||
function listingPrice(item: Listing) {
|
||||
return formatMoney(getListingDisplayPrice(item))
|
||||
}
|
||||
|
||||
function handleToggleFavorite() {
|
||||
if (!listing.value) return
|
||||
const added = toggleFavorite(listing.value)
|
||||
@@ -353,228 +406,412 @@ function goBack() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="pc-detail page" v-loading="loading">
|
||||
<div class="anti-fraud-strip compact">
|
||||
<span>防骗提示:下单后请按平台交接流程确认收号与归还,不要私下交易。</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-back-bar">
|
||||
<button class="detail-back-btn" type="button" @click="goBack">
|
||||
<section class="pc-detail-page" v-loading="loading">
|
||||
<!-- 顶部导航与安全保障条 -->
|
||||
<div class="top-nav-bar">
|
||||
<button class="back-pill-btn" type="button" @click="goBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
<span>返回列表</span>
|
||||
<span>返回租号大厅</span>
|
||||
</button>
|
||||
|
||||
<div class="safe-notice-strip">
|
||||
<van-icon name="shield-o" class="safe-icon" :size="16" />
|
||||
<span class="safe-text">平台官方担保:下单后请严格按照平台交付流程确认收号与归还,切勿脱离平台私下交易。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情主布局:左侧详尽档案 + 右侧粘性下单面板 -->
|
||||
<div v-if="listing" class="pc-detail-layout">
|
||||
<section class="pc-detail-main">
|
||||
<div class="detail-hero-card">
|
||||
<AuthImage
|
||||
v-if="coverURL"
|
||||
:source="coverURL"
|
||||
:alt="getListingTitle(listing)"
|
||||
image-class="detail-hero-img"
|
||||
loading="eager"
|
||||
/>
|
||||
<span v-else>HFB ACCOUNT</span>
|
||||
<div class="detail-hero-overlay">
|
||||
<div class="detail-tags">
|
||||
<span>{{ getServerRegion(listing) }}</span>
|
||||
<span v-if="getLoginMethod(listing)">{{ getLoginMethod(listing) }}</span>
|
||||
<span v-if="listing.rank_level">{{ listing.rank_level }}</span>
|
||||
</div>
|
||||
<button class="detail-code-chip" type="button" @click="copyListingCode">
|
||||
编号 {{ formatListingCode(listing) }}
|
||||
</button>
|
||||
<button class="detail-favorite-chip" type="button" @click="handleToggleFavorite">
|
||||
<el-icon>
|
||||
<component :is="listingFavorited ? StarFilled : Star" />
|
||||
</el-icon>
|
||||
{{ listingFavorited ? '已收藏' : '收藏账号' }}
|
||||
</button>
|
||||
<h1>{{ getListingTitle(listing) }}</h1>
|
||||
<p>{{ getListingSubtitle(listing) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 左侧内容流 -->
|
||||
<div class="pc-detail-main">
|
||||
<!-- 账号抬头与核心属性 -->
|
||||
<section class="detail-card header-card">
|
||||
<div class="header-meta-row">
|
||||
<div class="meta-tags-wrap">
|
||||
<button class="code-badge-btn" type="button" @click="copyListingCode" title="点击复制商品编号">
|
||||
<span>{{ formatListingCode(listing) }}</span>
|
||||
<el-icon :size="12"><DocumentCopy /></el-icon>
|
||||
</button>
|
||||
|
||||
<div class="detail-body">
|
||||
<div class="detail-summary-row">
|
||||
<div
|
||||
v-for="metric in detailMetrics"
|
||||
:key="metric.label"
|
||||
class="detail-metric"
|
||||
:class="`is-${metric.tone}`"
|
||||
>
|
||||
<span>{{ metric.label }}</span>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<span v-for="tag in accessTags" :key="tag.label" class="platform-pill" :class="tag.tone">
|
||||
<van-icon :name="tag.icon" :size="13" />
|
||||
<span>{{ tag.label }}</span>
|
||||
</span>
|
||||
|
||||
<section class="detail-section">
|
||||
<div class="detail-section-head">
|
||||
<h2>账号资料</h2>
|
||||
<span>{{ formatGameName(listing.game_name, '三角洲') }}</span>
|
||||
</div>
|
||||
<div class="detail-chip-row">
|
||||
<span v-for="chip in getListingChips(listing)" :key="chip.label">
|
||||
{{ chip.label }}:{{ chip.value }}
|
||||
<span v-if="listing.rank_level" class="platform-pill rank">
|
||||
<van-icon name="diamond-o" :size="13" />
|
||||
<span>{{ listing.rank_level }}</span>
|
||||
</span>
|
||||
|
||||
<span class="status-pill" :class="{ 'is-busy': listing.in_transaction }">
|
||||
<i class="status-dot" />
|
||||
<span>{{ listing.in_transaction ? '交易中' : '现号可租' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<dl class="detail-info-grid">
|
||||
<div v-for="row in accountRows" :key="row.label">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>{{ row.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="detail-section">
|
||||
<div class="detail-section-head">
|
||||
<h2>号主备注</h2>
|
||||
</div>
|
||||
<p class="detail-description">{{ listing.description || '号主暂未填写详细说明。' }}</p>
|
||||
</section>
|
||||
|
||||
<section v-if="getListingResources(listing).length" class="detail-section">
|
||||
<div class="detail-section-head">
|
||||
<h2>额外消耗品</h2>
|
||||
<span>{{ getListingResources(listing).length }} 项</span>
|
||||
</div>
|
||||
<div class="detail-resource-grid">
|
||||
<div
|
||||
v-for="resource in getListingResources(listing)"
|
||||
:key="resource.key"
|
||||
class="detail-resource-card"
|
||||
>
|
||||
<span>{{ resource.label }}</span>
|
||||
<strong>{{ resource.quantity }}</strong>
|
||||
<em>
|
||||
<b>{{ resource.mode || '--' }}</b>
|
||||
<small v-if="resource.amount > 0">¥{{ formatMoney(resource.amount) }}</small>
|
||||
<small v-else-if="resource.mode === '收费'">{{ resource.price || '¥0' }}</small>
|
||||
<small v-else>无额外收费</small>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detailSkinGroups.length" class="detail-section">
|
||||
<div class="detail-section-head">
|
||||
<h2>皮肤清单</h2>
|
||||
<span>按类型展示</span>
|
||||
</div>
|
||||
<div class="skin-groups">
|
||||
<div v-for="group in detailSkinGroups" :key="group.key" class="skin-group">
|
||||
<h3>{{ group.title }}</h3>
|
||||
<div class="detail-chip-row">
|
||||
<span v-for="skin in group.options" :key="skin">{{ skin }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detailScreenshots.length" class="detail-section">
|
||||
<div class="detail-section-head">
|
||||
<h2>账号截图</h2>
|
||||
<span>{{ detailScreenshots.length }} 张</span>
|
||||
</div>
|
||||
<div class="detail-screenshot-grid">
|
||||
<figure
|
||||
v-for="(shot, index) in detailScreenshots"
|
||||
:key="shot.url"
|
||||
class="detail-screenshot"
|
||||
>
|
||||
<AuthImage
|
||||
:source="shot.url"
|
||||
:alt="shot.label"
|
||||
image-class="detail-screenshot-image"
|
||||
fallback-class="detail-screenshot-image"
|
||||
fit="cover"
|
||||
:preview-src-list="detailScreenshotUrls"
|
||||
:preview-initial-index="index"
|
||||
/>
|
||||
<figcaption>{{ shot.label }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="order-panel pc-order-card">
|
||||
<div class="order-card-head">
|
||||
<div>
|
||||
<span class="order-eyebrow">平台担保</span>
|
||||
<h2>立即下单</h2>
|
||||
</div>
|
||||
<span class="order-state" :class="{ 'is-busy': listing.in_transaction }">
|
||||
{{ listing.in_transaction ? '交易中' : '可租' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="order-safe-text">平台托管订单与押金,按平台交接流程完成账号使用。</p>
|
||||
|
||||
<el-form label-position="top" class="order-form">
|
||||
<div class="order-total-box">
|
||||
<div class="order-total-head">
|
||||
<span>租赁合计</span>
|
||||
<strong>¥{{ listingPrice(listing) }}</strong>
|
||||
</div>
|
||||
<div class="order-price-breakdown">
|
||||
<div>
|
||||
<span>基础租金</span>
|
||||
<strong>¥{{ formatMoney(orderPriceBreakdown.rent) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>额外物品</span>
|
||||
<strong>¥{{ formatMoney(orderPriceBreakdown.consumable) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-deposit-row">
|
||||
<span>押金另付</span>
|
||||
<strong>¥{{ formatCent(listing.deposit_amount_cent) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="order-check-list">
|
||||
<div>
|
||||
<dt>账号区服</dt>
|
||||
<dd>{{ getServerRegion(listing) || '--' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>上号方式</dt>
|
||||
<dd>{{ getLoginMethod(listing) || '--' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>预计可租</dt>
|
||||
<dd>{{ formatEstimatedRentalDuration(listing) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="order-action-group">
|
||||
<el-button
|
||||
type="warning"
|
||||
size="large"
|
||||
:loading="ordering || agreementsLoading"
|
||||
:disabled="listing.in_transaction"
|
||||
class="full-control order-primary-btn"
|
||||
@click="handleCreateOrder"
|
||||
>
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
{{ listing.in_transaction ? '交易中' : '立即下单' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="full-control favorite-order-btn"
|
||||
size="large"
|
||||
<button
|
||||
class="favorite-action-btn"
|
||||
:class="{ 'is-active': listingFavorited }"
|
||||
type="button"
|
||||
@click="handleToggleFavorite"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon :size="15">
|
||||
<component :is="listingFavorited ? StarFilled : Star" />
|
||||
</el-icon>
|
||||
{{ listingFavorited ? '取消收藏' : '收藏账号' }}
|
||||
</el-button>
|
||||
<span>{{ listingFavorited ? '已收藏' : '收藏账号' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 标题与副标题 -->
|
||||
<div class="header-title-block">
|
||||
<h1 class="listing-main-title">{{ getListingTitle(listing) }}</h1>
|
||||
<p v-if="getListingSubtitle(listing)" class="listing-subtitle">
|
||||
{{ getListingSubtitle(listing) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 核心资产四大微卡(完全对齐首页) -->
|
||||
<div class="core-assets-bar">
|
||||
<!-- 哈夫币 -->
|
||||
<div class="asset-tile coin-tile">
|
||||
<span class="tile-label">哈夫币资产</span>
|
||||
<strong class="tile-value text-orange">{{ hafCoinDisplay }}</strong>
|
||||
</div>
|
||||
|
||||
<!-- 保险格数 -->
|
||||
<div class="asset-tile slot-tile">
|
||||
<span class="tile-label">安全箱格数</span>
|
||||
<strong class="tile-value text-slate">{{ insuranceSlot }}</strong>
|
||||
</div>
|
||||
|
||||
<!-- 折算币比 -->
|
||||
<div v-if="ratioDisplay" class="asset-tile ratio-tile">
|
||||
<span class="tile-label">折算币比</span>
|
||||
<strong class="tile-value text-amber">{{ ratioDisplay }}</strong>
|
||||
</div>
|
||||
|
||||
<!-- 租期与日耗 -->
|
||||
<div class="asset-tile rental-tile">
|
||||
<span class="tile-label">租用与损耗</span>
|
||||
<div class="tile-subvalues">
|
||||
<span class="val-pill green">可租 {{ rentalDuration }}</span>
|
||||
<span v-if="dailyLoss" class="val-pill warm">消耗 {{ dailyLoss }}/天</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 多图互动验号展台 (Gallery Showcase) -->
|
||||
<section class="detail-card gallery-card">
|
||||
<div class="section-title-bar">
|
||||
<div class="title-left">
|
||||
<h2>验号图片展台</h2>
|
||||
<span class="title-tip">真实游戏截图,点击可全屏高清放大查看细节</span>
|
||||
</div>
|
||||
<span class="gallery-count">共 {{ detailScreenshots.length }} 张截图</span>
|
||||
</div>
|
||||
|
||||
<!-- 主展台主图 -->
|
||||
<div class="gallery-stage">
|
||||
<div v-if="activeScreenshot" class="stage-viewer">
|
||||
<AuthImage
|
||||
:source="activeScreenshot.url"
|
||||
:alt="activeScreenshot.label"
|
||||
image-class="stage-image"
|
||||
fallback-class="stage-image"
|
||||
fit="contain"
|
||||
:preview-src-list="detailScreenshotUrls"
|
||||
:preview-initial-index="activeImageIndex"
|
||||
/>
|
||||
<!-- 左上角分类角标 -->
|
||||
<span class="stage-tag">{{ activeScreenshot.label }}</span>
|
||||
<!-- 右下角放大提示 -->
|
||||
<div class="stage-zoom-hint">
|
||||
<van-icon name="search" :size="13" />
|
||||
<span>点击放大查看</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="stage-empty">
|
||||
<span>暂无验号截图</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部缩略图胶卷导轨 -->
|
||||
<div v-if="detailScreenshots.length > 1" class="thumbnail-filmstrip">
|
||||
<button
|
||||
v-for="(shot, idx) in detailScreenshots"
|
||||
:key="shot.url + idx"
|
||||
type="button"
|
||||
class="thumbnail-item"
|
||||
:class="{ 'is-active': idx === activeImageIndex }"
|
||||
@click="activeImageIndex = idx"
|
||||
@mouseenter="activeImageIndex = idx"
|
||||
>
|
||||
<AuthImage
|
||||
:source="shot.url"
|
||||
:alt="shot.label"
|
||||
image-class="thumb-img"
|
||||
fallback-class="thumb-img"
|
||||
fit="cover"
|
||||
/>
|
||||
<span class="thumb-label">{{ shot.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 账号核心档案参数矩阵 -->
|
||||
<section class="detail-card specs-card">
|
||||
<div class="section-title-bar">
|
||||
<div class="title-left">
|
||||
<h2>账号核心档案</h2>
|
||||
<span class="title-tip">{{ formatGameName(listing.game_name, '三角洲行动') }} 资产详细规格</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 次要规格标签流 -->
|
||||
<div v-if="secondaryChips.length > 0" class="secondary-chips-flow">
|
||||
<span v-for="chip in secondaryChips" :key="chip.label" class="secondary-spec-pill">
|
||||
<span class="pill-k">{{ chip.label }}</span>
|
||||
<strong class="pill-v">{{ chip.value }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 12 宫格档案矩阵 -->
|
||||
<div class="specs-grid">
|
||||
<div v-for="row in accountRows" :key="row.label" class="spec-cell">
|
||||
<span class="cell-label">{{ row.label }}</span>
|
||||
<strong class="cell-val">{{ row.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 精选皮肤展台(若有) -->
|
||||
<section v-if="detailSkinGroups.length" class="detail-card skin-card">
|
||||
<div class="section-title-bar">
|
||||
<div class="title-left">
|
||||
<h2>精选皮肤展台</h2>
|
||||
<span class="title-tip">包含近战、金皮、红皮及武器皮肤</span>
|
||||
</div>
|
||||
<span class="gallery-count">共 {{ totalSkinCount }} 款精选</span>
|
||||
</div>
|
||||
|
||||
<div class="skin-groups-container">
|
||||
<div v-for="group in detailSkinGroups" :key="group.key" class="skin-group-block">
|
||||
<div class="skin-group-header">
|
||||
<span class="skin-group-title">{{ group.title }}</span>
|
||||
<span class="skin-group-count">{{ group.options.length }} 款</span>
|
||||
</div>
|
||||
<div class="skin-pill-wrap">
|
||||
<span
|
||||
v-for="opt in group.options"
|
||||
:key="opt"
|
||||
class="skin-pill"
|
||||
:class="`skin-type-${group.type}`"
|
||||
>
|
||||
<i class="quality-dot" />
|
||||
<span>{{ opt }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 仓库贵重物资 & 额外消耗品(若有) -->
|
||||
<section v-if="resourceItems.length" class="detail-card resources-card">
|
||||
<div class="section-title-bar">
|
||||
<div class="title-left">
|
||||
<h2>仓库物资与消耗品</h2>
|
||||
<span class="title-tip">随号附带的仓库高价值装备物资</span>
|
||||
</div>
|
||||
<span class="gallery-count">{{ resourceItems.length }} 种物资</span>
|
||||
</div>
|
||||
|
||||
<div class="resources-grid">
|
||||
<div v-for="res in resourceItems" :key="res.key" class="resource-tile">
|
||||
<div class="res-top">
|
||||
<span class="res-name">{{ res.label }}</span>
|
||||
<strong class="res-qty">{{ formatResourceQuantity(res) }}</strong>
|
||||
</div>
|
||||
<div class="res-bottom">
|
||||
<span v-if="res.mode === '赠送'" class="res-badge gift">随号赠送</span>
|
||||
<span v-else-if="res.mode === '收费'" class="res-badge charge">
|
||||
额外收费 <small v-if="res.amount > 0">¥{{ formatMoney(res.amount) }}</small>
|
||||
</span>
|
||||
<span v-else class="res-badge standard">{{ res.mode || '账号包内' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 号主说明与交接备注 -->
|
||||
<section class="detail-card note-card">
|
||||
<div class="section-title-bar">
|
||||
<div class="title-left">
|
||||
<h2>号主留言与说明</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="seller-note-box">
|
||||
<van-icon name="chat-o" :size="18" class="note-quote-icon" />
|
||||
<p class="note-content">{{ listing.description || '号主暂未填写特殊交接要求,可正常直接下单租用。' }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 平台四大安全保障 -->
|
||||
<section class="detail-card trust-card">
|
||||
<div class="section-title-bar">
|
||||
<div class="title-left">
|
||||
<h2>平台官方交易保障</h2>
|
||||
<span class="title-tip">哈夫币租号平台全面护航您的每次交易</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trust-features-grid">
|
||||
<div class="trust-item">
|
||||
<div class="trust-icon-box orange">
|
||||
<van-icon name="shield-o" :size="20" />
|
||||
</div>
|
||||
<div class="trust-text">
|
||||
<strong>官方资金托管</strong>
|
||||
<p>租金与押金全程平台监管,确认收号无误后才结算</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trust-item">
|
||||
<div class="trust-icon-box blue">
|
||||
<van-icon name="passed" :size="20" />
|
||||
</div>
|
||||
<div class="trust-text">
|
||||
<strong>极速自动退押</strong>
|
||||
<p>租期结束按规范归还并确认无损后,押金原路秒退</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trust-item">
|
||||
<div class="trust-icon-box green">
|
||||
<van-icon name="service-o" :size="20" />
|
||||
</div>
|
||||
<div class="trust-text">
|
||||
<strong>专人客服协助</strong>
|
||||
<p>上号交接若遇异常,平台官方客服随时介入协助</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trust-item">
|
||||
<div class="trust-icon-box amber">
|
||||
<van-icon name="certificate" :size="20" />
|
||||
</div>
|
||||
<div class="trust-text">
|
||||
<strong>双向实名认证</strong>
|
||||
<p>买卖双方均通过公安实名比对校验,杜绝欺诈</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:常驻粘性结算下单面板 -->
|
||||
<aside class="pc-order-panel">
|
||||
<div class="order-sticky-box">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="panel-header">
|
||||
<div class="panel-title-wrap">
|
||||
<span class="panel-eyebrow">平台托管交接</span>
|
||||
<h2 class="panel-title">立即租用</h2>
|
||||
</div>
|
||||
<span class="order-status-pill" :class="{ 'is-busy': listing.in_transaction }">
|
||||
<i class="dot" />
|
||||
<span>{{ listing.in_transaction ? '交易中' : '现号可租' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 价格与费用结构总览(精美暖色渐变卡) -->
|
||||
<div class="checkout-price-card">
|
||||
<div class="price-header-row">
|
||||
<span class="price-caption">租赁合计</span>
|
||||
<div class="price-total-display">
|
||||
<small>¥</small>
|
||||
<strong>{{ orderTotal }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 费用拆解 -->
|
||||
<div class="price-breakdown-list">
|
||||
<div class="breakdown-row">
|
||||
<span class="b-label">账号基础租金</span>
|
||||
<span class="b-val">¥{{ formatMoney(orderPriceBreakdown.rent) }}</span>
|
||||
</div>
|
||||
<div v-if="orderPriceBreakdown.consumable > 0" class="breakdown-row">
|
||||
<span class="b-label">随号额外物资</span>
|
||||
<span class="b-val">¥{{ formatMoney(orderPriceBreakdown.consumable) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 押金行 -->
|
||||
<div class="deposit-guarantee-row">
|
||||
<div class="dep-left">
|
||||
<span class="dep-title">押金另付</span>
|
||||
<small class="dep-sub">归还验收无损自动原路退还</small>
|
||||
</div>
|
||||
<strong class="dep-val">
|
||||
{{ listing.deposit_amount_cent > 0 ? `¥${formatCent(listing.deposit_amount_cent)}` : '免押金' }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 规格速览确认清单 -->
|
||||
<div class="order-quick-summary">
|
||||
<div class="summary-item">
|
||||
<span class="s-label">区服方式</span>
|
||||
<strong class="s-val">{{ getServerRegion(listing) }} · {{ getLoginMethod(listing) }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="s-label">预计可租</span>
|
||||
<strong class="s-val text-green">{{ formatEstimatedRentalDuration(listing) }}</strong>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="s-label">方便上号</span>
|
||||
<strong class="s-val">{{ getOnlineTimeText(listing) || '--' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下单操作区 -->
|
||||
<div class="order-cta-block">
|
||||
<button
|
||||
type="button"
|
||||
class="primary-order-btn"
|
||||
:disabled="listing.in_transaction || ordering || agreementsLoading"
|
||||
@click="handleCreateOrder"
|
||||
>
|
||||
<el-icon :size="18"><ShoppingCart /></el-icon>
|
||||
<span>{{ listing.in_transaction ? '该账号交易中' : '立即下单租用' }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="secondary-favorite-btn"
|
||||
:class="{ 'is-active': listingFavorited }"
|
||||
@click="handleToggleFavorite"
|
||||
>
|
||||
<el-icon :size="16">
|
||||
<component :is="listingFavorited ? StarFilled : Star" />
|
||||
</el-icon>
|
||||
<span>{{ listingFavorited ? '取消收藏' : '收藏账号' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全信任脚标 -->
|
||||
<div class="order-panel-trust-footer">
|
||||
<div class="trust-badge-row">
|
||||
<span><van-icon name="passed" /> 平台托管</span>
|
||||
<span><van-icon name="passed" /> 极速验号</span>
|
||||
<span><van-icon name="passed" /> 秒退押金</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- 下单协议确认弹窗 (保持完整业务逻辑) -->
|
||||
<el-dialog
|
||||
v-model="agreementVisible"
|
||||
title="下单协议确认"
|
||||
@@ -583,7 +820,7 @@ function goBack() {
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="agreements" class="agreement-dialog-body">
|
||||
<p class="agreement-tip">请完整阅读并勾选以下两份协议后继续下单。</p>
|
||||
<p class="agreement-tip">请完整阅读并勾选以下两份协议后继续下单:</p>
|
||||
<div class="agreement-grid">
|
||||
<section class="agreement-panel">
|
||||
<h3>{{ agreements.virtual_asset_purchase.title }}</h3>
|
||||
|
||||
Reference in New Issue
Block a user