diff --git a/frontend/src/features/admin/views/AdminListingDetailView.vue b/frontend/src/features/admin/views/AdminListingDetailView.vue index 9610ef7..236e0ad 100644 --- a/frontend/src/features/admin/views/AdminListingDetailView.vue +++ b/frontend/src/features/admin/views/AdminListingDetailView.vue @@ -7,6 +7,7 @@ import { useRoute } from 'vue-router' import { fetchAdminUsers, type AdminUserItem } from '@/features/admin/api/adminUsers' import { fetchAdminFileBlob } from '@/shared/api/files' +import AuthImage from '@/shared/components/business/AuthImage.vue' import { adminPath } from '@/shared/utils/adminPath' import { formatCentWithSymbol } from '@/shared/utils/money' import { @@ -23,7 +24,21 @@ import { userStatusLabel, } from '@/shared/utils/statusLabels' import { formatDateTime } from '@/shared/utils/time' -import { formatListingCode } from '@/shared/utils/listingDisplay' +import { + assetRegions, + formatEstimatedRentalDuration, + formatHafCoinM, + getCoinWan, + getDailyLoss, + getListingConsumablePrice, + getListingResources, + getOnlineTimeText, + getResourceQuantity, + getSkinNames, + formatListingCode, + readAssetNumber, + readAssetString, +} from '@/shared/utils/listingDisplay' const route = useRoute() const loading = ref(false) @@ -51,6 +66,77 @@ const canTransfer = computed(() => !!listing.value && listing.value.status !== ' const selectedUserAvailable = computed( () => selectedUser.value?.status === 'active' && selectedUser.value.realname_status === 'verified' ) +const assetRows = computed(() => { + if (!listing.value) return [] + return [ + { label: '商品编号', value: formatListingCode(listing.value) }, + { label: '账号 ID', value: listing.value.account_id }, + { label: '游戏', value: listing.value.game_name || '-' }, + { label: '区服', value: listing.value.server_region || '-' }, + { label: '平台', value: listing.value.login_platform || '-' }, + { label: '段位', value: listing.value.rank_level || '-' }, + { label: '烽火等级', value: assetNumberText(listing.value, 'fire_level') }, + { label: '保险格数', value: assetText(listing.value, 'season_insurance') }, + { + label: '体力/负重', + value: `${assetText(listing.value, 'stamina_level')} / ${assetText(listing.value, 'load_level')}`, + }, + { label: '绝密 KD', value: assetNumberText(listing.value, 'secret_kd') }, + { label: '常用地区', value: regionText(listing.value) }, + { label: '在线时间', value: getOnlineTimeText(listing.value) || '-' }, + ] +}) +const ownerRows = computed(() => { + if (!listing.value) return [] + return [ + { + label: '号主', + value: listing.value.owner_phone || listing.value.owner_nickname || listing.value.owner_id, + }, + { label: '号主 ID', value: listing.value.owner_id }, + { label: '价格', value: listingPrice(listing.value) }, + { label: '押金', value: moneyCent(listing.value.deposit_amount_cent) }, + { label: '哈夫币', value: formatHafCoinM(getCoinWan(listing.value)) }, + { label: '每日损耗', value: getDailyLoss(listing.value) || '-' }, + { label: '预计可租', value: formatEstimatedRentalDuration(listing.value) }, + { + label: '额外消耗', + value: moneyCent(Math.round(getListingConsumablePrice(listing.value) * 100)), + }, + ] +}) +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 skinNames = computed(() => (listing.value ? getSkinNames(listing.value).slice(0, 16) : [])) +const hiddenSkinCount = computed(() => { + if (!listing.value) return 0 + return Math.max(0, getSkinNames(listing.value).length - skinNames.value.length) +}) +const statusTone = computed(() => { + if (!listing.value) return 'info' + if (listing.value.status === 'published') return 'success' + if (['offline', 'abnormal'].includes(listing.value.status)) return 'danger' + if (listing.value.status === 'rented') return 'warning' + return 'info' +}) +const reviewTone = computed(() => { + if (!listing.value) return 'info' + if (listing.value.review_status === 'approved') return 'success' + if (listing.value.review_status === 'rejected') return 'danger' + if (listing.value.review_status === 'pending') return 'warning' + return 'info' +}) onMounted(loadListing) @@ -158,18 +244,42 @@ function listingPrice(row: Listing) { return moneyCent(row.price_cent) } +function assetText(row: Listing, key: string) { + const value = readAssetString(row, key) + if (value) return value + const numberValue = readAssetNumber(row, key) + return numberValue > 0 ? String(numberValue) : '-' +} + +function assetNumberText(row: Listing, key: string) { + const value = readAssetNumber(row, key) + if (value <= 0) return '-' + const rounded = Math.round(value * 10) / 10 + return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1) +} + +function regionText(row: Listing) { + const regions = assetRegions(row) + return regions.length ? regions.join('、') : '-' +} + function extractObjectKey(url: string) { try { const parsed = new URL(url, window.location.origin) - return parsed.searchParams.get('key') || url + return parsed.searchParams.get('key') || '' } catch { - return url + return '' } } async function openScreenshot(url: string) { try { - const blob = await fetchAdminFileBlob(extractObjectKey(url)) + const key = extractObjectKey(url) + if (!key) { + window.open(url, '_blank') + return + } + const blob = await fetchAdminFileBlob(key) window.open(URL.createObjectURL(blob), '_blank') } catch { window.open(url, '_blank') @@ -179,18 +289,30 @@ async function openScreenshot(url: string) {