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) }}
+
+
+
+ {{ faceOwnerText(row) }}
+
+
+
{{ allHeroText(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;
}