-
- {{ activeListing.title }}
-
-
-
- {{ reason }}
-
-
-
+
+
+
+ #{{ formatListingCode(selectedListing) }}
+ {{ selectedListing.title }}
+ 售价 {{ money(buyerTotalPrice(selectedListing)) }}
+ 🚨 存在封禁
-
- 取消
- 确认拒绝
-
-
-
-
-
-
-
- 卖家发布价
- {{ money(sellerTotalPrice(priceAdjustListing)) }}
-
-
- 调整后买家价
- {{
- previewMoney(priceAdjustPreview?.buyerTotalPrice || 0)
- }}
-
-
- 调整后买家比例
- {{
- ratioText(priceAdjustPreview?.buyerRatio || 0)
- }}
-
-
-
- 按加价后比例
- 按加价后价格
-
-
-
-
-
-
- 取消
- 保存调整
+
+ 调整价格 (P)
+
+
-
-
-
-
-
-
- {{ evidenceListing.title }}
-
-
+
-
-
-
暂无截图
+ 通过并上架 (A)
+
+
+ 下一单 (J)
+
-
- 关闭
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/features/listings/api/listings.ts b/frontend/src/features/listings/api/listings.ts
index def5474..8c20f9b 100644
--- a/frontend/src/features/listings/api/listings.ts
+++ b/frontend/src/features/listings/api/listings.ts
@@ -332,3 +332,19 @@ export async function rejectListing(id: number, reason: string) {
})
return data.data
}
+
+export interface AdminListingScreenshotsPayload {
+ screenshot_urls: string[]
+ screenshot_groups?: Record
+}
+
+export async function adminUpdateListingScreenshots(
+ id: number,
+ payload: AdminListingScreenshotsPayload
+) {
+ const { data } = await apiClient.put>(
+ `/admin/listings/${id}/screenshots`,
+ payload
+ )
+ return data.data
+}
diff --git a/frontend/src/features/listings/components/HomeBanner.vue b/frontend/src/features/listings/components/HomeBanner.vue
index 890e6c7..308009c 100644
--- a/frontend/src/features/listings/components/HomeBanner.vue
+++ b/frontend/src/features/listings/components/HomeBanner.vue
@@ -10,8 +10,15 @@ interface HomeBannerSlide {
image_url?: string
}
+interface StatItem {
+ label: string
+ value: string
+ hint?: string
+}
+
interface Props {
banners: HomeBannerSlide[]
+ stats?: StatItem[]
}
const props = defineProps()
@@ -45,7 +52,7 @@ function hasSlideCopy(slide: HomeBannerSlide) {
-
+
+
+
+
+ {{ st.value }}
+ {{ st.label }}
+
+
+
.hero-board {
+ position: relative;
min-width: 0;
- border-radius: 16px;
+ width: 100%;
+ border-radius: 14px;
overflow: hidden;
background: #ffffff;
}
@@ -98,7 +115,7 @@ function hasSlideCopy(slide: HomeBannerSlide) {
}
.hero-board :deep(.el-carousel__container) {
- height: 240px;
+ height: 200px;
overflow: hidden;
}
@@ -111,7 +128,7 @@ function hasSlideCopy(slide: HomeBannerSlide) {
height: 100%;
display: flex;
align-items: center;
- padding: 40px;
+ padding: 24px 36px;
overflow: hidden;
color: #17233d;
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 58%, #fff7ed 100%);
@@ -160,8 +177,8 @@ function hasSlideCopy(slide: HomeBannerSlide) {
.hero-copy span {
color: #ff6a00;
- font-size: 13px;
- font-weight: 900;
+ font-size: 12px;
+ font-weight: 700;
}
.hero-slide.has-image .hero-copy {
@@ -173,35 +190,76 @@ function hasSlideCopy(slide: HomeBannerSlide) {
}
.hero-copy h1 {
- margin: 12px 0;
- font-size: 32px;
- font-weight: 900;
+ margin: 6px 0;
+ font-size: 22px;
+ font-weight: 800;
line-height: 1.2;
}
.hero-copy p {
display: inline-block;
margin: 0;
- padding: 6px 12px;
+ padding: 3px 10px;
border-radius: 999px;
color: #1e293b;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(4px);
- font-size: 14px;
- font-weight: 800;
+ font-size: 12px;
+ font-weight: 700;
}
.hero-slide em {
position: absolute;
- top: 22px;
- right: 24px;
+ top: 14px;
+ right: 18px;
z-index: 1;
- padding: 6px 10px;
+ padding: 4px 9px;
border-radius: 999px;
color: #ffffff;
background: #ff6a00;
- font-size: 12px;
+ font-size: 11px;
font-style: normal;
- font-weight: 900;
+ font-weight: 700;
+}
+
+/* 悬浮微型统计标签栏 */
+.banner-stat-chips {
+ position: absolute;
+ bottom: 12px;
+ right: 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ z-index: 3;
+}
+
+.stat-pill {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 5px;
+ padding: 4px 12px;
+ border-radius: 999px;
+ background: rgba(15, 23, 42, 0.65);
+ backdrop-filter: blur(6px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ color: #ffffff;
+ font-size: 12px;
+}
+
+.stat-pill-val {
+ font-size: 14px;
+ font-weight: 700;
+ color: #fb923c;
+}
+
+.stat-pill-lbl {
+ font-size: 11px;
+ color: #cbd5e1;
+}
+
+@media (max-width: 768px) {
+ .banner-stat-chips {
+ display: none;
+ }
}
diff --git a/frontend/src/features/listings/components/HomeZones.vue b/frontend/src/features/listings/components/HomeZones.vue
index 09254d2..49ee55e 100644
--- a/frontend/src/features/listings/components/HomeZones.vue
+++ b/frontend/src/features/listings/components/HomeZones.vue
@@ -35,134 +35,79 @@ function selectZone(key: string) {
-
+
diff --git a/frontend/src/features/listings/components/ListingCard.vue b/frontend/src/features/listings/components/ListingCard.vue
index 1c43230..1307b42 100644
--- a/frontend/src/features/listings/components/ListingCard.vue
+++ b/frontend/src/features/listings/components/ListingCard.vue
@@ -18,6 +18,10 @@ import {
hasAcceleratedSaleRatio,
hasGiftResources,
getRatioValue,
+ getCoinWan,
+ formatHafCoinM,
+ readAssetString,
+ isTopTierInsurance,
type ListingDisplayResource,
} from '@/shared/utils/listingDisplay'
@@ -27,36 +31,50 @@ interface Props {
const props = defineProps
()
+// 封面图片:优先使用后端生成的公开缩略图(16KB秒开,免鉴权)
const coverURL = computed(() => props.listing.cover_url || props.listing.screenshot_urls?.[0] || '')
+
+// 基础元信息
+const listingCode = computed(() => formatListingCode(props.listing))
const dailyLoss = computed(() => getDailyLoss(props.listing))
-const skinNames = computed(() => getSkinNames(props.listing))
-const skinGroups = computed(() => getListingSkinGroups(props.listing))
-const skinSummaryText = computed(() =>
- skinGroups.value.map(group => `${group.title}: ${group.options.join(' / ')}`).join(' | ')
-)
-const chipItems = computed(() => getListingChips(props.listing))
-const resourceItems = computed(() => getListingResources(props.listing))
-const resourceSummaryText = computed(() =>
- resourceItems.value
- .map(resource => `${resource.label} ${formatResourceQuantity(resource)}`)
- .join('、')
-)
-const hasDetailLines = computed(() => skinGroups.value.length > 0 || resourceItems.value.length > 0)
const rentalDuration = computed(() => formatEstimatedRentalDuration(props.listing))
+const ratioValue = computed(() => getRatioValue(props.listing))
+
+// 核心三大指标(突出买家最关心的资产)
+const hafCoinDisplay = computed(() => formatHafCoinM(getCoinWan(props.listing)))
+const insuranceSlot = computed(() => readAssetString(props.listing, 'season_insurance') || '--')
+const ratioDisplay = computed(() => {
+ if (ratioValue.value <= 0) return null
+ return `1元 = ${ratioValue.value.toFixed(0)}w`
+})
+
+// 平台/区服标签
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))
+
+// 次要属性胶囊(过滤掉已在核心资产栏呈现的哈夫币、保险,以及已在右侧仓库展台呈现的 AWM 等物资)
+const allChips = computed(() => getListingChips(props.listing))
+const secondaryChips = computed(() => {
+ return allChips.value.filter(
+ chip => chip.label !== '哈夫币' && chip.label !== '保险格数' && chip.label !== 'AWM'
+ )
+})
+
+// 皮肤与物资
+const skinNames = computed(() => getSkinNames(props.listing))
+const skinGroups = computed(() => getListingSkinGroups(props.listing))
+const fullSkinSummary = computed(() =>
+ skinGroups.value.map(group => `${group.title}:${group.options.join(' / ')}`).join('\n')
+)
+
+const resourceItems = computed(() => getListingResources(props.listing))
+const fullResourceSummary = computed(() =>
+ resourceItems.value
+ .map(resource => `${resource.label} ${formatResourceQuantity(resource)}`)
+ .join('、')
)
function formatResourceQuantity(resource: ListingDisplayResource) {
@@ -81,489 +99,775 @@ function formatStatNumber(value: number) {
-
-