feat(listing): 展示本人人脸信息

This commit is contained in:
yml
2026-11-11 17:02:03 +08:00
parent c960a5caef
commit a5601eb76c
13 changed files with 182 additions and 12 deletions
@@ -758,6 +758,16 @@
color: #b91c1c;
}
.spec-item.face-owner-warning {
border-color: #fca5a5;
background: #fef2f2;
}
.spec-item.face-owner-warning .spec-label,
.spec-item.face-owner-warning .spec-val {
color: #b91c1c;
}
.copyable-val {
display: flex;
align-items: center;
@@ -51,11 +51,13 @@ import {
formatListingCode,
formatHafCoinM,
getCoinWan,
getFaceOwnerText,
getListingConsumablePrice,
getListingResources,
getOnlineTimeText,
getResourceQuantity,
getSkinNames,
isNonOwnFace,
readAssetNumber,
readAssetString,
} from '@/shared/utils/listingDisplay'
@@ -1388,6 +1390,13 @@ async function handleScreenshotSaved(payload: { oldUrl: string; newUrl: string }
{{ assetText(selectedListing, 'load_level') }}
</strong>
</div>
<div
class="spec-item"
:class="{ 'face-owner-warning': isNonOwnFace(selectedListing) }"
>
<span class="spec-label">是否本人人脸</span>
<strong class="spec-val">{{ getFaceOwnerText(selectedListing) }}</strong>
</div>
<div
class="spec-item spec-item--full"
:class="{ 'ban-warning': hasBanRecord(selectedListing) }"
@@ -23,6 +23,7 @@ import {
formatRatio,
getCoinWan,
getDailyLoss,
getFaceOwnerText,
getListingRentPrice,
getResourceQuantity,
getSkinGroup,
@@ -150,6 +151,7 @@ const screenshotColumns = [
width: 420,
read: (row: Listing) => characterAndWeaponSkinText(row),
},
{ label: '人脸', width: 60, read: (row: Listing) => faceOwnerText(row) },
{ label: '全英雄', width: 74, read: (row: Listing) => allHeroText(row) },
{ label: '号主备注', width: 160, read: (row: Listing) => ownerRemarkText(row) },
{ label: '租客纯币价格', width: 105, read: (row: Listing) => renterPureCoinPriceText(row) },
@@ -607,6 +609,10 @@ function ownerRemarkText(row: Listing) {
return remark || '-'
}
function faceOwnerText(row: Listing) {
return getFaceOwnerText(row)
}
/** 全英雄为发布必填项;老数据缺失时显示占位符。 */
function allHeroText(row: Listing) {
const value = assetText(row, 'all_hero')
@@ -996,6 +1002,13 @@ function formatQuantity(value: number) {
{{ characterAndWeaponSkinText(row) }}
</template>
</el-table-column>
<el-table-column label="人脸" width="60">
<template #default="{ row }">
<span :class="{ 'face-owner-no': faceOwnerText(row) === '否' }">
{{ faceOwnerText(row) }}
</span>
</template>
</el-table-column>
<el-table-column label="全英雄" width="74">
<template #default="{ row }">{{ allHeroText(row) }}</template>
</el-table-column>
@@ -8,6 +8,7 @@ import {
getDailyLoss,
getListingDisplayPrice,
formatListingCode,
getFaceOwnerText,
getListingChips,
getListingResources,
getListingSkinGroups,
@@ -22,6 +23,7 @@ import {
formatHafCoinM,
readAssetString,
isTopTierInsurance,
isNonOwnFace,
type ListingDisplayResource,
} from '@/shared/utils/listingDisplay'
@@ -39,6 +41,7 @@ const listingCode = computed(() => formatListingCode(props.listing))
const dailyLoss = computed(() => getDailyLoss(props.listing))
const rentalDuration = computed(() => formatEstimatedRentalDuration(props.listing))
const ratioValue = computed(() => getRatioValue(props.listing))
const faceOwner = computed(() => getFaceOwnerText(props.listing))
// 核心三大指标(突出买家最关心的资产)
const hafCoinDisplay = computed(() => formatHafCoinM(getCoinWan(props.listing)))
@@ -154,6 +157,10 @@ function formatStatNumber(value: number) {
<span class="aux-label">消耗</span>
<strong class="aux-val">{{ dailyLoss }}/天</strong>
</span>
<span class="aux-badge face-badge" :class="{ 'is-non-owner': isNonOwnFace(listing) }">
<span class="aux-label">本人人脸</span>
<strong class="aux-val">{{ faceOwner }}</strong>
</span>
</div>
<div class="key-assets-strip">
@@ -571,6 +578,27 @@ function formatStatNumber(value: number) {
color: #b45309;
}
/* 人脸归属:本人使用蓝色,非本人使用红色提醒 */
.face-badge {
background: #eff6ff;
border-color: #dbeafe;
color: #2563eb;
}
.face-badge .aux-label {
color: #3b82f6;
}
.face-badge.is-non-owner {
background: #fef2f2;
border-color: #fecaca;
color: #b91c1c;
}
.face-badge.is-non-owner .aux-label {
color: #dc2626;
}
/* 核心资产三大卡片(统一家底灰阶底色,品牌橙为主高光,整体干净高级) */
.key-assets-strip {
display: flex;
@@ -294,16 +294,16 @@
color: #64748b;
}
/* 核心资产:有币比时五个并排,没有币比时四个并排 */
/* 核心资产:有币比时六个并排,没有币比时五个并排 */
.core-assets-bar {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.core-assets-bar.has-ratio {
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.asset-tile {
@@ -336,6 +336,25 @@
border-color: #fdba74;
}
.face-tile {
background: #eff6ff;
border-color: #dbeafe;
}
.face-tile .tile-value {
color: #2563eb;
}
.face-tile.is-non-owner {
background: #fef2f2;
border-color: #fecaca;
}
.face-tile.is-non-owner .tile-label,
.face-tile.is-non-owner .tile-value {
color: #b91c1c;
}
.tile-label {
font-size: 12px;
font-weight: 500;
@@ -1189,22 +1208,17 @@
.core-assets-bar,
.core-assets-bar.has-ratio {
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.core-assets-bar .asset-tile {
grid-column: span 3;
grid-column: span 1;
}
.core-assets-bar.has-ratio .asset-tile {
.core-assets-bar .face-tile {
grid-column: span 2;
}
.core-assets-bar.has-ratio .rental-tile,
.core-assets-bar.has-ratio .loss-tile {
grid-column: span 3;
}
.specs-grid {
grid-template-columns: repeat(2, 1fr);
}
@@ -26,6 +26,7 @@ import {
formatRatio,
getCoinWan,
getDailyLoss,
getFaceOwnerText,
getListingConsumablePrice,
getListingChips,
getListingDisplayPrice,
@@ -40,6 +41,7 @@ import {
readAssetString,
getRatioValue,
isTopTierInsurance,
isNonOwnFace,
getScreenshotThumbURL,
getScreenshotMediumURL,
getScreenshotRawURL,
@@ -138,6 +140,11 @@ const dailyLoss = computed(() => {
return getDailyLoss(listing.value)
})
const faceOwner = computed(() => {
if (!listing.value) return '--'
return getFaceOwnerText(listing.value)
})
// 平台/区服特性徽标
const accessTags = computed(() => {
if (!listing.value) return []
@@ -526,6 +533,14 @@ function goBack() {
<strong class="tile-value text-amber">{{ ratioDisplay }}</strong>
</div>
<div
class="asset-tile face-tile"
:class="{ 'is-non-owner': listing && isNonOwnFace(listing) }"
>
<span class="tile-label">是否本人人脸</span>
<strong class="tile-value">{{ faceOwner }}</strong>
</div>
<div class="asset-tile rental-tile">
<span class="tile-label">可租</span>
<strong class="tile-value text-green">{{ rentalDuration }}</strong>
@@ -938,6 +938,18 @@
border: 1px solid #fef3c7;
}
.aux-pill.face {
background: #eff6ff;
color: #2563eb;
border: 1px solid #dbeafe;
}
.aux-pill.face.is-non-owner {
background: #fef2f2;
color: #b91c1c;
border-color: #fecaca;
}
/* 中部:封面 + 信息流 */
.mobile-card-body {
display: grid;
@@ -54,6 +54,7 @@ import {
getListingChips,
getListingDisplayPrice,
getDailyLoss,
getFaceOwnerText,
getRatioValue,
formatListingCode,
formatEstimatedRentalDuration,
@@ -63,6 +64,7 @@ import {
hasAcceleratedSaleRatio,
hasGiftResources,
isTopTierInsurance,
isNonOwnFace,
getCoinWan,
formatHafCoinM,
readAssetString,
@@ -1146,6 +1148,9 @@ if (!restoreHomeConfigFromCache()) {
<span v-if="getDailyLoss(item)" class="aux-pill loss">
耗 {{ getDailyLoss(item) }}/天
</span>
<span class="aux-pill face" :class="{ 'is-non-owner': isNonOwnFace(item) }">
人脸 {{ getFaceOwnerText(item) }}
</span>
</div>
</div>
@@ -290,7 +290,7 @@
.core-assets-bar.has-ratio .rental-tile,
.core-assets-bar.has-ratio .loss-tile {
grid-column: span 3;
grid-column: span 2;
}
.asset-tile {
@@ -323,6 +323,26 @@
border-color: #fdba74;
}
.face-tile {
grid-column: span 2;
background: #eff6ff;
border-color: #dbeafe;
}
.face-tile .tile-value {
color: #2563eb;
}
.face-tile.is-non-owner {
background: #fef2f2;
border-color: #fecaca;
}
.face-tile.is-non-owner .tile-label,
.face-tile.is-non-owner .tile-value {
color: #b91c1c;
}
.tile-label {
font-size: 11px;
font-weight: 500;
@@ -24,6 +24,7 @@ import {
formatRatio,
getCoinWan,
getDailyLoss,
getFaceOwnerText,
getListingConsumablePrice,
getListingChips,
getListingDisplayPrice,
@@ -38,6 +39,7 @@ import {
readAssetString,
getRatioValue,
isTopTierInsurance,
isNonOwnFace,
getScreenshotThumbURL,
getScreenshotMediumURL,
getScreenshotRawURL,
@@ -136,6 +138,11 @@ const dailyLoss = computed(() => {
return getDailyLoss(listing.value)
})
const faceOwner = computed(() => {
if (!listing.value) return '--'
return getFaceOwnerText(listing.value)
})
// 平台/区服特性徽标
const accessTags = computed(() => {
if (!listing.value) return []
@@ -498,6 +505,14 @@ function goBack() {
<strong class="tile-value text-slate">{{ ratioDisplay }}</strong>
</div>
<div
class="asset-tile face-tile"
:class="{ 'is-non-owner': listing && isNonOwnFace(listing) }"
>
<span class="tile-label">是否本人人脸</span>
<strong class="tile-value">{{ faceOwner }}</strong>
</div>
<div class="asset-tile rental-tile">
<span class="tile-label">可租</span>
<strong class="tile-value text-green">{{ rentalDuration }}</strong>
@@ -1,10 +1,12 @@
import { describe, expect, it } from 'vitest'
import {
getFaceOwnerText,
getListingChips,
getListingResources,
getListingSellerCoinPrice,
getListingSellerPrice,
isNonOwnFace,
} from '../listingDisplay'
import type { PublicListingItem } from '@/features/listings/api/listings'
@@ -84,6 +86,19 @@ describe('首页规格芯片', () => {
})
})
describe('本人人脸展示', () => {
it('读取发布时填写的本人人脸状态', () => {
expect(getFaceOwnerText(buildListing({ face_owner: '是' }))).toBe('是')
expect(getFaceOwnerText(buildListing({ face_owner: '否' }))).toBe('否')
})
it('非本人人脸可单独识别,老数据回退占位符', () => {
expect(isNonOwnFace(buildListing({ face_owner: '否' }))).toBe(true)
expect(isNonOwnFace(buildListing({ face_owner: '是' }))).toBe(false)
expect(getFaceOwnerText(buildListing())).toBe('--')
})
})
describe('首页规格芯片 · 德穆兰优先级高于全英雄', () => {
it('解锁德穆兰时用「解锁德穆兰」替换「全英雄」', () => {
const chips = getListingChips(buildListing({ all_hero: '是', unlock_demulan: '是' }))
@@ -107,6 +107,15 @@ export function getServerRegion(item: PublicListingItem) {
return item.server_region.trim()
}
export function getFaceOwnerText(item: PublicListingItem) {
const value = readAssetString(item, 'face_owner').trim()
return value === '是' || value === '否' ? value : '--'
}
export function isNonOwnFace(item: PublicListingItem) {
return getFaceOwnerText(item) === '否'
}
// 把后端机读游戏名转成中文展示名。
export function formatGameName(value: unknown, fallback = '-') {
const text = String(value || '').trim()
+5
View File
@@ -1169,6 +1169,11 @@ h1 {
word-break: keep-all;
}
.admin-listings-table .face-owner-no {
color: #dc2626;
font-weight: 700;
}
.admin-listings-table.el-table .el-table__cell {
padding: 8px 0;
}