feat(admin-listings): 商品列表与详情补充账号属性并展示赠送资产

This commit is contained in:
yml2213
2026-10-17 15:20:32 +08:00
parent 790267e3eb
commit e37684d401
4 changed files with 125 additions and 14 deletions
@@ -180,6 +180,31 @@
background: #f8fafc;
}
.resource-item.is-gift {
border-color: #fdba74;
background: #fff7ed;
}
.resource-item.is-gift strong {
color: #c2410c;
}
.resource-value {
display: flex;
align-items: center;
gap: 6px;
}
.resource-gift-tag {
padding: 1px 6px;
border-radius: 999px;
background: #ffedd5;
color: #c2410c;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.resource-item span {
color: #64748b;
font-size: 13px;
@@ -140,6 +140,17 @@ const assetRows = computed(() => {
{ label: '绝密 KD', value: assetNumberText(listing.value, 'secret_kd') },
{ label: '常用地区', value: regionText(listing.value) },
{ label: '在线时间', value: getOnlineTimeText(listing.value) || '-' },
{
label: '是否可以修改游戏名',
value: assetText(listing.value, 'can_change_game_name'),
},
{ label: '是否有全英雄', value: assetText(listing.value, 'all_hero') },
{ label: '是否解锁德穆兰', value: assetText(listing.value, 'unlock_demulan') },
{
label: '凌晨0-2点可响应上号',
value: assetText(listing.value, 'early_morning_response'),
},
{ label: '封禁记录', value: assetText(listing.value, 'ban_record') },
]
})
const ownerRows = computed(() => {
@@ -178,17 +189,21 @@ const ownerRows = computed(() => {
})
const resourceCards = computed(() => {
if (!listing.value) return []
const core = [
{ key: 'awmAmmo', label: 'AWM 子弹', quantity: getResourceQuantity(listing.value, 'awmAmmo') },
{ key: 'helmet6', label: '六级头', quantity: getResourceQuantity(listing.value, 'helmet6') },
{ key: 'armor6', label: '六级甲', quantity: getResourceQuantity(listing.value, 'armor6') },
].filter(item => item.quantity > 0)
const extra = getListingResources(listing.value)
.filter(item => !core.some(coreItem => coreItem.key === item.key))
.slice(0, 6)
.map(item => ({ key: item.key, label: item.label, quantity: item.quantity }))
return [...core, ...extra]
const labelOverrides: Record<string, string> = {
awmAmmo: 'AWM 子弹',
helmet6: '六级头',
armor6: '六级甲',
}
return getListingResources(listing.value).map(item => ({
key: item.key,
label: labelOverrides[item.key] || item.label,
quantity: item.quantity,
mode: item.mode,
}))
})
const giftResourceCount = computed(
() => resourceCards.value.filter(item => item.mode === '赠送').length
)
const skinNames = computed(() => (listing.value ? getSkinNames(listing.value).slice(0, 16) : []))
const hiddenSkinCount = computed(() => {
if (!listing.value) return 0
@@ -839,14 +854,24 @@ async function openScreenshot(url: string) {
<div class="panel-heading">
<h2>库存资产</h2>
<span
>{{ resourceCards.length }} 项资源 ·
{{ skinNames.length + hiddenSkinCount }} 个皮肤</span
>{{ resourceCards.length }} 项资源<template v-if="giftResourceCount">
· {{ giftResourceCount }} 项赠送</template
>
· {{ skinNames.length + hiddenSkinCount }} 个皮肤</span
>
</div>
<div v-if="resourceCards.length" class="resource-grid">
<div v-for="item in resourceCards" :key="item.key" class="resource-item">
<div
v-for="item in resourceCards"
:key="item.key"
class="resource-item"
:class="{ 'is-gift': item.mode === '赠送' }"
>
<span>{{ item.label }}</span>
<strong>{{ item.quantity }}</strong>
<div class="resource-value">
<strong>{{ item.quantity }}</strong>
<small v-if="item.mode === '赠送'" class="resource-gift-tag">赠送</small>
</div>
</div>
</div>
<p v-else class="empty-line">暂无库存资源</p>
@@ -155,6 +155,7 @@ const screenshotColumns = [
width: 420,
read: (row: Listing) => characterAndWeaponSkinText(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) },
{
@@ -639,6 +640,12 @@ function ownerRemarkText(row: Listing) {
return remark || '-'
}
/** 全英雄为发布必填项;老数据缺失时显示占位符。 */
function allHeroText(row: Listing) {
const value = assetText(row, 'all_hero')
return value === '是' || value === '否' ? value : '-'
}
function rentAndDepositText(row: Listing) {
return `${listingPrice(row)}/${formatCentWithSymbol(row.deposit_amount_cent)}`
}
@@ -1024,6 +1031,9 @@ function formatQuantity(value: number) {
{{ characterAndWeaponSkinText(row) }}
</template>
</el-table-column>
<el-table-column label="全英雄" width="74">
<template #default="{ row }">{{ allHeroText(row) }}</template>
</el-table-column>
<el-table-column label="号主备注" min-width="150">
<template #default="{ row }">{{ ownerRemarkText(row) }}</template>
</el-table-column>
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
getListingChips,
getListingResources,
getListingSellerCoinPrice,
getListingSellerPrice,
} from '../listingDisplay'
@@ -114,3 +115,53 @@ describe('首页规格芯片 · 德穆兰优先级高于全英雄', () => {
expect(chips.find(chip => chip.label === '全英雄')).toEqual({ label: '全英雄', value: '是' })
})
})
describe('库存资源解析', () => {
it('保留赠送资产及其计费模式,便于后台详情单独标记', () => {
const resources = getListingResources(
buildListing({
resources: [
{
key: 'awmAmmo',
label: 'AWM子弹',
price: '0.6元/发',
quantity: 31,
mode: '收费',
},
{
key: 'giftMed',
label: '赠送医疗物资',
price: '1元/个',
quantity: 4,
mode: '赠送',
},
],
})
)
expect(resources).toHaveLength(2)
expect(resources.find(item => item.key === 'giftMed')).toMatchObject({
label: '赠送医疗物资',
quantity: 4,
mode: '赠送',
amount: 0,
})
expect(resources.find(item => item.key === 'awmAmmo')).toMatchObject({
mode: '收费',
amount: 18.6,
})
})
it('过滤数量为 0 的资产', () => {
const resources = getListingResources(
buildListing({
resources: [
{ key: 'zero', label: '零数量物资', price: '1元/个', quantity: 0, mode: '赠送' },
{ key: 'gift', label: '赠送物资', price: '1元/个', quantity: 1, mode: '赠送' },
],
})
)
expect(resources.map(item => item.key)).toEqual(['gift'])
})
})