diff --git a/frontend/src/features/admin/views/AdminListingReviewView.css b/frontend/src/features/admin/views/AdminListingReviewView.css index 8edacb39..1522246e 100644 --- a/frontend/src/features/admin/views/AdminListingReviewView.css +++ b/frontend/src/features/admin/views/AdminListingReviewView.css @@ -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; diff --git a/frontend/src/features/admin/views/AdminListingReviewView.vue b/frontend/src/features/admin/views/AdminListingReviewView.vue index a10494e2..54eaf1f3 100644 --- a/frontend/src/features/admin/views/AdminListingReviewView.vue +++ b/frontend/src/features/admin/views/AdminListingReviewView.vue @@ -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') }} +
+ 是否本人人脸 + {{ getFaceOwnerText(selectedListing) }} +
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) }} + + + diff --git a/frontend/src/features/listings/components/ListingCard.vue b/frontend/src/features/listings/components/ListingCard.vue index 74cb4bad..19250b5c 100644 --- a/frontend/src/features/listings/components/ListingCard.vue +++ b/frontend/src/features/listings/components/ListingCard.vue @@ -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) { 消耗 {{ dailyLoss }}/天 + + 本人人脸 + {{ faceOwner }} +
@@ -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; diff --git a/frontend/src/features/listings/views/ListingDetailView.css b/frontend/src/features/listings/views/ListingDetailView.css index e411bb4d..282c6178 100644 --- a/frontend/src/features/listings/views/ListingDetailView.css +++ b/frontend/src/features/listings/views/ListingDetailView.css @@ -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); } diff --git a/frontend/src/features/listings/views/ListingDetailView.vue b/frontend/src/features/listings/views/ListingDetailView.vue index 44430525..63a59e60 100644 --- a/frontend/src/features/listings/views/ListingDetailView.vue +++ b/frontend/src/features/listings/views/ListingDetailView.vue @@ -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() { {{ ratioDisplay }}
+
+ 是否本人人脸 + {{ faceOwner }} +
+
可租 {{ rentalDuration }} diff --git a/frontend/src/features/listings/views/MobileHomeView.css b/frontend/src/features/listings/views/MobileHomeView.css index 1f8965f7..be25f027 100644 --- a/frontend/src/features/listings/views/MobileHomeView.css +++ b/frontend/src/features/listings/views/MobileHomeView.css @@ -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; diff --git a/frontend/src/features/listings/views/MobileHomeView.vue b/frontend/src/features/listings/views/MobileHomeView.vue index 7f43e3b1..08fbfb74 100644 --- a/frontend/src/features/listings/views/MobileHomeView.vue +++ b/frontend/src/features/listings/views/MobileHomeView.vue @@ -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()) { 耗 {{ getDailyLoss(item) }}/天 + + 人脸 {{ getFaceOwnerText(item) }} +
diff --git a/frontend/src/features/listings/views/MobileListingDetailView.css b/frontend/src/features/listings/views/MobileListingDetailView.css index cc7ded58..92268c8f 100644 --- a/frontend/src/features/listings/views/MobileListingDetailView.css +++ b/frontend/src/features/listings/views/MobileListingDetailView.css @@ -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; diff --git a/frontend/src/features/listings/views/MobileListingDetailView.vue b/frontend/src/features/listings/views/MobileListingDetailView.vue index 347f741a..317ab83e 100644 --- a/frontend/src/features/listings/views/MobileListingDetailView.vue +++ b/frontend/src/features/listings/views/MobileListingDetailView.vue @@ -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() { {{ ratioDisplay }} +
+ 是否本人人脸 + {{ faceOwner }} +
+
可租 {{ rentalDuration }} diff --git a/frontend/src/shared/utils/__tests__/listingDisplay.spec.ts b/frontend/src/shared/utils/__tests__/listingDisplay.spec.ts index e2156909..9b69431f 100644 --- a/frontend/src/shared/utils/__tests__/listingDisplay.spec.ts +++ b/frontend/src/shared/utils/__tests__/listingDisplay.spec.ts @@ -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: '是' })) diff --git a/frontend/src/shared/utils/listingDisplay.ts b/frontend/src/shared/utils/listingDisplay.ts index 0f656704..181e841b 100644 --- a/frontend/src/shared/utils/listingDisplay.ts +++ b/frontend/src/shared/utils/listingDisplay.ts @@ -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() diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index c3dbe988..70a46438 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -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; }