feat(listing): 优化商品管理表格列布局
This commit is contained in:
@@ -55,8 +55,28 @@ const fallbackLevelOptions = ['1级', '2级', '3级', '4级', '5级', '6级', '7
|
||||
|
||||
const pageSize = ref(10)
|
||||
const currentPage = ref(1)
|
||||
const pageRef = ref<HTMLElement | null>(null)
|
||||
const listingTableWidth = ref(1296)
|
||||
const activeFilterPopover = ref<FilterPopover | ''>('')
|
||||
const publishOptions = ref(emptyListingPublishOptions)
|
||||
const listingColumnUnits = {
|
||||
code: 70,
|
||||
zone: 75,
|
||||
region: 105,
|
||||
core: 85,
|
||||
level: 60,
|
||||
equipment: 80,
|
||||
skin: 145,
|
||||
face: 60,
|
||||
remark: 85,
|
||||
price: 100,
|
||||
period: 55,
|
||||
ratio: 35,
|
||||
detail: 45,
|
||||
} as const
|
||||
type ListingColumnKey = keyof typeof listingColumnUnits
|
||||
const listingColumnWidths = computed(() => allocateListingColumnWidths(listingTableWidth.value))
|
||||
let listingResizeObserver: ResizeObserver | undefined
|
||||
|
||||
const {
|
||||
loading,
|
||||
@@ -133,35 +153,39 @@ const skinFilterLabel = computed(() => {
|
||||
})
|
||||
|
||||
const screenshotColumns = [
|
||||
{ label: '商品编码', width: 88, read: (row: Listing) => formatListingCode(row) },
|
||||
{ label: '地区', width: 92, read: (row: Listing) => regionText(row) },
|
||||
{ label: '上号方式', width: 78, read: (row: Listing) => row.login_platform || '-' },
|
||||
{ label: '哈夫币(M)', width: 86, read: (row: Listing) => listingCoinM(row) },
|
||||
{ label: '保险', width: 56, read: (row: Listing) => assetText(row, 'season_insurance') },
|
||||
{ label: '体力(级)', width: 70, read: (row: Listing) => levelText(row, 'stamina_level') },
|
||||
{ label: '负重(级)', width: 70, read: (row: Listing) => levelText(row, 'load_level') },
|
||||
{ label: '账号等级', width: 76, read: (row: Listing) => assetText(row, 'fire_level') },
|
||||
{ label: '绝密KD', width: 66, read: (row: Listing) => assetText(row, 'secret_kd') },
|
||||
{ label: 'awm子弹', width: 74, read: (row: Listing) => resourceText(row, 'awmAmmo') },
|
||||
{ label: '六头', width: 50, read: (row: Listing) => resourceText(row, 'helmet6') },
|
||||
{ label: '六甲', width: 50, read: (row: Listing) => resourceText(row, 'armor6') },
|
||||
{ label: '特殊刀皮', width: 110, read: (row: Listing) => skinGroupText(row, 'melee') },
|
||||
{ label: '商品编码', width: 98, read: (row: Listing) => formatListingCode(row) },
|
||||
{
|
||||
label: '人物红皮/人物金皮/武器皮肤',
|
||||
width: 420,
|
||||
read: (row: Listing) => characterAndWeaponSkinText(row),
|
||||
label: '专区/赔付',
|
||||
width: 105,
|
||||
read: (row: Listing) => zoneCompensationText(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) },
|
||||
{
|
||||
label: '租期',
|
||||
width: 92,
|
||||
read: (row: Listing) => rentalPeriodText(row),
|
||||
label: '地区/上号/纯币',
|
||||
width: 147,
|
||||
read: (row: Listing) => regionLoginCoinText(row),
|
||||
},
|
||||
{ label: '租金/押金', width: 96, read: (row: Listing) => rentAndDepositText(row) },
|
||||
{ label: '比例', width: 64, read: (row: Listing) => formatRatio(row) },
|
||||
{
|
||||
label: '体力/负重/保险',
|
||||
width: 119,
|
||||
read: (row: Listing) => coreAssetText(row),
|
||||
},
|
||||
{ label: '等级/KD', width: 84, read: (row: Listing) => levelKdText(row) },
|
||||
{
|
||||
label: 'AWM/六头/六甲',
|
||||
width: 112,
|
||||
read: (row: Listing) => equipmentText(row),
|
||||
},
|
||||
{ label: '皮肤资产', width: 203, read: (row: Listing) => skinAssetText(row) },
|
||||
{ label: '人脸/全英雄', width: 84, read: (row: Listing) => faceHeroText(row) },
|
||||
{ label: '号主备注', width: 119, read: (row: Listing) => ownerRemarkText(row) },
|
||||
{
|
||||
label: '纯币/租金/押金',
|
||||
width: 140,
|
||||
read: (row: Listing) => priceRentDepositText(row),
|
||||
},
|
||||
{ label: '租期', width: 77, read: (row: Listing) => rentalPeriodText(row) },
|
||||
{ label: '比例', width: 49, read: (row: Listing) => formatRatio(row) },
|
||||
{ label: '详情', width: 63, read: () => '详情' },
|
||||
] as const
|
||||
|
||||
async function queryListings() {
|
||||
@@ -273,6 +297,7 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
observeListingTableWidth()
|
||||
restoreListState()
|
||||
void loadPublishOptions()
|
||||
await loadListings()
|
||||
@@ -282,11 +307,38 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
saveListState()
|
||||
listingResizeObserver?.disconnect()
|
||||
adminMainEl()?.removeEventListener('scroll', handleAdminMainScroll)
|
||||
if (stateSaveTimer !== undefined) window.clearTimeout(stateSaveTimer)
|
||||
if (scrollSaveTimer !== undefined) window.clearTimeout(scrollSaveTimer)
|
||||
})
|
||||
|
||||
function observeListingTableWidth() {
|
||||
const element = pageRef.value
|
||||
if (!element) return
|
||||
listingTableWidth.value = Math.max(element.clientWidth, 960)
|
||||
listingResizeObserver = new ResizeObserver(entries => {
|
||||
const width = entries[0]?.contentRect.width || element.clientWidth
|
||||
listingTableWidth.value = Math.max(Math.round(width), 960)
|
||||
})
|
||||
listingResizeObserver.observe(element)
|
||||
}
|
||||
|
||||
function allocateListingColumnWidths(totalWidth: number) {
|
||||
const width = Math.max(Math.round(totalWidth), 960)
|
||||
const keys = Object.keys(listingColumnUnits) as ListingColumnKey[]
|
||||
const widths = {} as Record<ListingColumnKey, number>
|
||||
let usedWidth = 0
|
||||
for (const key of keys) {
|
||||
if (key === 'detail') continue
|
||||
const columnWidth = Math.floor((width * listingColumnUnits[key]) / 1000)
|
||||
widths[key] = columnWidth
|
||||
usedWidth += columnWidth
|
||||
}
|
||||
widths.detail = Math.max(width - usedWidth, 44)
|
||||
return widths
|
||||
}
|
||||
|
||||
async function loadPublishOptions() {
|
||||
try {
|
||||
publishOptions.value = await fetchListingPublishOptions()
|
||||
@@ -409,7 +461,6 @@ function renderTableScreenshotCanvas(rows: Listing[]) {
|
||||
const paddingX = 24
|
||||
const paddingY = 20
|
||||
const titleHeight = 44
|
||||
const headerHeight = 34
|
||||
const lineHeight = 18
|
||||
const cellPaddingX = 8
|
||||
const cellPaddingY = 10
|
||||
@@ -430,6 +481,11 @@ function renderTableScreenshotCanvas(rows: Listing[]) {
|
||||
const maxLines = Math.max(...lineGroups.map(lines => lines.length), 1)
|
||||
return Math.max(44, maxLines * lineHeight + cellPaddingY * 2)
|
||||
})
|
||||
const headerLines = screenshotColumns.map(column =>
|
||||
wrapCanvasText(measureCtx, column.label, column.width - cellPaddingX * 2)
|
||||
)
|
||||
const maxHeaderLines = Math.max(...headerLines.map(lines => lines.length), 1)
|
||||
const headerHeight = Math.max(34, maxHeaderLines * lineHeight + cellPaddingY * 2)
|
||||
const canvasHeight =
|
||||
paddingY * 2 + titleHeight + headerHeight + rowHeights.reduce((sum, height) => sum + height, 0)
|
||||
const canvas = document.createElement('canvas')
|
||||
@@ -459,8 +515,14 @@ function renderTableScreenshotCanvas(rows: Listing[]) {
|
||||
ctx.font = '700 12px Arial, "Microsoft YaHei", sans-serif'
|
||||
ctx.fillStyle = '#8f9bba'
|
||||
let x = paddingX
|
||||
for (const column of screenshotColumns) {
|
||||
drawCellText(ctx, [column.label], x + cellPaddingX, y + 22, lineHeight)
|
||||
for (const [columnIndex, column] of screenshotColumns.entries()) {
|
||||
drawCellText(
|
||||
ctx,
|
||||
headerLines[columnIndex] ?? [column.label],
|
||||
x + cellPaddingX,
|
||||
y + cellPaddingY + 12,
|
||||
lineHeight
|
||||
)
|
||||
x += column.width
|
||||
}
|
||||
drawLine(ctx, paddingX, y + headerHeight, paddingX + tableWidth, y + headerHeight)
|
||||
@@ -579,6 +641,52 @@ function regionText(row: Listing) {
|
||||
return regions.length ? regions.join('、') : '-'
|
||||
}
|
||||
|
||||
function zoneCompensationText(row: Listing) {
|
||||
const zone = row.is_premium ? '极品' : '普通'
|
||||
const compensation =
|
||||
row.compensation_plan === 'insurance'
|
||||
? `无忧保险 ${(Number(row.insurance_fee_bps || 0) / 100).toFixed(1)}%`
|
||||
: '普通赔付'
|
||||
return `${zone}\n${compensation}`
|
||||
}
|
||||
|
||||
function regionLoginCoinText(row: Listing) {
|
||||
return [regionText(row), row.login_platform || '-', listingCoinM(row)].join('\n')
|
||||
}
|
||||
|
||||
function insuranceSlotText(row: Listing) {
|
||||
const value = assetText(row, 'season_insurance')
|
||||
return value === '-' ? '-' : insuranceSlotLabel(value)
|
||||
}
|
||||
|
||||
function coreAssetText(row: Listing) {
|
||||
return [
|
||||
levelText(row, 'stamina_level'),
|
||||
levelText(row, 'load_level'),
|
||||
insuranceSlotText(row),
|
||||
].join('/')
|
||||
}
|
||||
|
||||
function coreAssetTitle(row: Listing) {
|
||||
const insurance = assetText(row, 'season_insurance')
|
||||
const slots = insurance === '-' ? '-' : insuranceSlotLabel(insurance)
|
||||
const insuranceText =
|
||||
insurance === '-' || slots === insurance ? insurance : `${insurance}=${slots}格`
|
||||
return `体力 ${levelText(row, 'stamina_level')} / 负重 ${levelText(row, 'load_level')} / 保险 ${insuranceText}`
|
||||
}
|
||||
|
||||
function levelKdText(row: Listing) {
|
||||
return [assetText(row, 'fire_level'), assetText(row, 'secret_kd')].join('/')
|
||||
}
|
||||
|
||||
function equipmentText(row: Listing) {
|
||||
return [
|
||||
resourceText(row, 'awmAmmo'),
|
||||
resourceText(row, 'helmet6'),
|
||||
resourceText(row, 'armor6'),
|
||||
].join('/')
|
||||
}
|
||||
|
||||
function resourceText(row: Listing, key: string) {
|
||||
const quantity = getResourceQuantity(row, key)
|
||||
return quantity > 0 ? formatQuantity(quantity) : '-'
|
||||
@@ -589,8 +697,9 @@ function skinGroupText(row: Listing, groupKey: string) {
|
||||
return skins.length ? skins.join('、') : '-'
|
||||
}
|
||||
|
||||
function characterAndWeaponSkinText(row: Listing) {
|
||||
function skinAssetText(row: Listing) {
|
||||
const groups = [
|
||||
{ label: '刀', key: 'melee' },
|
||||
{ label: '红皮', key: 'operatorRed' },
|
||||
{ label: '金皮', key: 'operatorGold' },
|
||||
{ label: '武器', key: 'weapon' },
|
||||
@@ -601,7 +710,19 @@ function characterAndWeaponSkinText(row: Listing) {
|
||||
return names.length ? `${group.label}:${names.join('、')}` : ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
return parts.length ? parts.join(' / ') : '-'
|
||||
return parts.length ? parts.join('\n') : '-'
|
||||
}
|
||||
|
||||
function faceHeroText(row: Listing) {
|
||||
return [faceOwnerText(row), allHeroText(row)].join('/')
|
||||
}
|
||||
|
||||
function priceRentDepositText(row: Listing) {
|
||||
return [
|
||||
renterPureCoinPriceText(row),
|
||||
listingPrice(row),
|
||||
formatCentWithSymbol(row.deposit_amount_cent),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function ownerRemarkText(row: Listing) {
|
||||
@@ -619,10 +740,6 @@ function allHeroText(row: Listing) {
|
||||
return value === '是' || value === '否' ? value : '-'
|
||||
}
|
||||
|
||||
function rentAndDepositText(row: Listing) {
|
||||
return `${listingPrice(row)}/${formatCentWithSymbol(row.deposit_amount_cent)}`
|
||||
}
|
||||
|
||||
// 租客纯币价格:不含额外消耗品的纯币部分(快照 buyer_coin_base_price 或 总价减消耗品)。
|
||||
function renterPureCoinPriceText(row: Listing) {
|
||||
const price = getListingRentPrice(row)
|
||||
@@ -649,7 +766,7 @@ function formatQuantity(value: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page admin-listings-page">
|
||||
<section ref="pageRef" class="page admin-listings-page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Listings</p>
|
||||
@@ -942,99 +1059,102 @@ function formatQuantity(value: number) {
|
||||
stripe
|
||||
empty-text="暂无商品"
|
||||
>
|
||||
<el-table-column label="商品编码" width="88">
|
||||
<el-table-column label="商品编码" :width="listingColumnWidths.code">
|
||||
<template #default="{ row }">
|
||||
<strong class="listing-code">{{ formatListingCode(row) }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="专区" width="72">
|
||||
<el-table-column
|
||||
label="专区/赔付"
|
||||
:width="listingColumnWidths.zone"
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_premium" type="warning" size="small">极品</el-tag>
|
||||
<span v-else>普通</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="赔付方案" width="102">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.compensation_plan === 'insurance'">
|
||||
无忧保险 {{ (Number(row.insurance_fee_bps || 0) / 100).toFixed(1) }}%
|
||||
</span>
|
||||
<span v-else>普通赔付</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地区" width="86">
|
||||
<template #default="{ row }">{{ regionText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="login_platform" label="上号方式" width="78" />
|
||||
<el-table-column label="哈夫币(M)" width="82">
|
||||
<template #default="{ row }">{{ listingCoinM(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="保险" width="54">
|
||||
<template #default="{ row }">{{ assetText(row, 'season_insurance') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="体力(级)" width="68">
|
||||
<template #default="{ row }">{{ levelText(row, 'stamina_level') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负重(级)" width="68">
|
||||
<template #default="{ row }">{{ levelText(row, 'load_level') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号等级" width="74">
|
||||
<template #default="{ row }">{{ assetText(row, 'fire_level') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绝密KD" width="66">
|
||||
<template #default="{ row }">{{ assetText(row, 'secret_kd') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="awm子弹" width="72">
|
||||
<template #default="{ row }">{{ resourceText(row, 'awmAmmo') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="六头" width="48">
|
||||
<template #default="{ row }">{{ resourceText(row, 'helmet6') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="六甲" width="48">
|
||||
<template #default="{ row }">{{ resourceText(row, 'armor6') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="特殊刀皮" width="104">
|
||||
<template #default="{ row }">
|
||||
{{ skinGroupText(row, 'melee') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人物红皮/人物金皮/武器皮肤" min-width="226">
|
||||
<template #default="{ row }">
|
||||
{{ 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 class="listing-multiline-cell" :class="{ 'listing-premium-text': row.is_premium }">
|
||||
{{ zoneCompensationText(row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="全英雄" width="74">
|
||||
<template #default="{ row }">{{ allHeroText(row) }}</template>
|
||||
<el-table-column
|
||||
label="地区/上号/纯币"
|
||||
:width="listingColumnWidths.region"
|
||||
show-overflow-tooltip
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="listing-multiline-cell">{{ regionLoginCoinText(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="号主备注" min-width="150">
|
||||
<el-table-column
|
||||
label="体力/负重/保险"
|
||||
:width="listingColumnWidths.core"
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :title="coreAssetTitle(row)">{{ coreAssetText(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="等级/KD"
|
||||
:width="listingColumnWidths.level"
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">{{ levelKdText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="AWM/六头/六甲"
|
||||
:width="listingColumnWidths.equipment"
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">{{ equipmentText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="皮肤资产"
|
||||
:width="listingColumnWidths.skin"
|
||||
show-overflow-tooltip
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="listing-multiline-cell">{{ skinAssetText(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="人脸/全英雄"
|
||||
:width="listingColumnWidths.face"
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
:class="{
|
||||
'face-owner-no': faceOwnerText(row) === '否',
|
||||
}"
|
||||
>
|
||||
{{ faceHeroText(row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="号主备注" :width="listingColumnWidths.remark" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ ownerRemarkText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="租客纯币价格" width="105">
|
||||
<el-table-column
|
||||
label="纯币/租金/押金"
|
||||
:width="listingColumnWidths.price"
|
||||
label-class-name="listing-merged-header"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :title="`租客纯币价格(不含消耗品)${renterPureCoinPriceText(row)}`">
|
||||
{{ renterPureCoinPriceText(row) }}
|
||||
</span>
|
||||
<span class="listing-multiline-cell">{{ priceRentDepositText(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="租期" width="82">
|
||||
<el-table-column label="租期" :width="listingColumnWidths.period">
|
||||
<template #default="{ row }">
|
||||
<span :title="estimateTitle(row)">{{ formatEstimatedRentalDuration(row) }}</span>
|
||||
<span v-if="getDailyLoss(row)" class="table-subtext">日耗 {{ getDailyLoss(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="租金/押金" width="90">
|
||||
<template #default="{ row }">{{ rentAndDepositText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="比例" width="62">
|
||||
<el-table-column label="比例" :width="listingColumnWidths.ratio">
|
||||
<template #default="{ row }">{{ formatRatio(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="详情" width="60">
|
||||
<el-table-column label="详情" :width="listingColumnWidths.detail">
|
||||
<template #default="{ row }">
|
||||
<RouterLink :to="adminPath(`listings/${row.id}`)">
|
||||
<el-button size="small">详情</el-button>
|
||||
|
||||
@@ -1161,6 +1161,12 @@ h1 {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.admin-listings-table.el-table th.listing-merged-header .cell {
|
||||
white-space: normal;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-listings-table .listing-code {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
@@ -1174,6 +1180,15 @@ h1 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-listings-table .listing-premium-text {
|
||||
color: #d97706;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-listings-table .listing-multiline-cell {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.admin-listings-table.el-table .el-table__cell {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user