优化了后台管理界面,

This commit is contained in:
yml2213
2026-05-24 01:01:04 +08:00
parent 5e422bfa40
commit 25e5599e0a
6 changed files with 643 additions and 106 deletions
@@ -5,6 +5,19 @@ import { useRoute } from 'vue-router'
import { fetchAdminFileBlob } from '@/api/files'
import { adminMarkListingAbnormal, adminOfflineListing, fetchAdminListing, type Listing } from '@/api/listings'
import {
formatMoney,
formatRatio,
getAdminListingPricing,
listingAssetTags,
listingBadgeTags,
listingBuyerPrice,
listingDisplayTitle,
listingResourceTags,
listingRiskTags,
listingSellerPrice,
ownerName,
} from '@/views/admin/listingAdminDisplay'
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
@@ -53,14 +66,6 @@ async function submitAction() {
}
}
function money(value: number) {
return `¥${Number(value || 0).toFixed(2)}`
}
function listingPrice(row: Listing) {
return money(row.price_daily || row.price_hourly)
}
function extractObjectKey(url: string) {
try {
const parsed = new URL(url, window.location.origin)
@@ -94,7 +99,7 @@ function readError(error: unknown, fallback: string) {
<div class="page-header">
<p class="eyebrow">Listing #{{ listing.id }}</p>
<h1>商品详情</h1>
<p>{{ listing.title }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
<p>{{ listingDisplayTitle(listing) }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
</div>
<div class="toolbar-actions">
<RouterLink to="/admin/listings">
@@ -115,18 +120,20 @@ function readError(error: unknown, fallback: string) {
<strong>{{ listingReviewStatusLabel(listing.review_status) }}</strong>
</div>
<div class="metric-card">
<span>价格</span>
<strong>{{ listingPrice(listing) }}</strong>
<span>买家日租</span>
<strong>{{ listingBuyerPrice(listing) }}</strong>
</div>
<div class="metric-card">
<span>押金</span>
<strong>{{ money(listing.deposit_amount) }}</strong>
<strong>{{ formatMoney(listing.deposit_amount) }}</strong>
</div>
</div>
<div v-if="listing" class="dashboard-panels">
<div class="order-panel dashboard-panel">
<h2>账号信息</h2>
<p>展示标题{{ listingDisplayTitle(listing) }}</p>
<p>原始标题{{ listing.title }}</p>
<p>账号 ID{{ listing.account_id }}</p>
<p>游戏{{ listing.game_name }}</p>
<p>区服{{ listing.server_region }}</p>
@@ -134,17 +141,58 @@ function readError(error: unknown, fallback: string) {
<p>段位{{ listing.rank_level || '-' }}</p>
<p>哈夫币{{ listing.haf_coin_amount }}</p>
<p>资产截图{{ listing.screenshot_urls?.length || 0 }} </p>
<div v-if="listingBadgeTags(listing).length" class="admin-tag-row">
<el-tag
v-for="tag in listingBadgeTags(listing)"
:key="tag"
size="small"
:type="tag === '特惠' ? 'danger' : 'warning'"
effect="plain"
>
{{ tag }}
</el-tag>
</div>
</div>
<div class="order-panel dashboard-panel">
<h2>号主与价格</h2>
<p>号主{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p>
<p>号主{{ ownerName(listing) }}</p>
<p>号主 ID{{ listing.owner_id }}</p>
<p>价格{{ listingPrice(listing) }}</p>
<p>押金{{ money(listing.deposit_amount) }}</p>
<p>买家日租{{ listingBuyerPrice(listing) }}</p>
<p>卖家价格{{ listingSellerPrice(listing) }}</p>
<p>押金{{ formatMoney(listing.deposit_amount) }}</p>
<p>卖家比例{{ formatRatio(getAdminListingPricing(listing).sellerRatio) }}</p>
<p>买家比例{{ formatRatio(getAdminListingPricing(listing).buyerRatio) }}</p>
<p>平台加价{{ formatMoney(getAdminListingPricing(listing).platformMarkup) }}</p>
</div>
</div>
<div v-if="listing" class="dashboard-panels">
<div class="order-panel dashboard-panel">
<h2>资产配置</h2>
<div class="admin-tag-row">
<el-tag v-for="tag in listingAssetTags(listing)" :key="tag" effect="plain">{{ tag }}</el-tag>
</div>
</div>
<div class="order-panel dashboard-panel">
<h2>交接风控</h2>
<div class="admin-tag-row">
<el-tag v-for="tag in listingRiskTags(listing)" :key="tag" type="info" effect="plain">{{ tag }}</el-tag>
</div>
</div>
</div>
<div v-if="listing" class="order-panel dashboard-panel">
<h2>额外资源</h2>
<div v-if="listingResourceTags(listing, 99).length" class="admin-tag-row">
<el-tag v-for="tag in listingResourceTags(listing, 99)" :key="tag" type="info" effect="plain">
{{ tag }}
</el-tag>
</div>
<p v-else>暂无额外资源</p>
</div>
<div v-if="listing" class="order-panel dashboard-panel">
<h2>说明与审核原因</h2>
<p>{{ listing.description || '暂无商品说明' }}</p>
@@ -4,6 +4,18 @@ import { onMounted, ref } from 'vue'
import { fetchAdminFileBlob } from '@/api/files'
import { approveListing, fetchPendingReviewListings, rejectListing, type Listing } from '@/api/listings'
import {
formatMoney,
formatRatio,
getAdminListingPricing,
listingAssetTags,
listingBadgeTags,
listingBuyerPrice,
listingDisplayTitle,
listingResourceTags,
listingSellerPrice,
ownerName,
} from '@/views/admin/listingAdminDisplay'
import { formatDateTime } from '@/utils/time'
const loading = ref(false)
@@ -62,10 +74,6 @@ function openEvidence(row: Listing) {
evidenceListing.value = row
}
function listingPrice(row: Listing) {
return `¥${Number(row.price_daily || row.price_hourly || 0).toFixed(2)}`
}
function extractObjectKey(url: string) {
try {
const parsed = new URL(url, window.location.origin)
@@ -105,16 +113,60 @@ function readError(error: unknown, fallback: string) {
</div>
<el-table v-loading="loading" class="table-panel" :data="listings">
<el-table-column prop="title" label="标题" min-width="180" />
<el-table-column prop="owner_id" label="号主" width="90" />
<el-table-column prop="server_region" label="区服" width="120" />
<el-table-column prop="login_platform" label="平台" width="120" />
<el-table-column prop="haf_coin_amount" label="哈夫币" width="110" />
<el-table-column prop="rank_level" label="段位" width="110" />
<el-table-column label="价格" width="90">
<template #default="{ row }">{{ listingPrice(row) }}</template>
<el-table-column label="商品" min-width="270" fixed="left">
<template #default="{ row }">
<strong>{{ listingDisplayTitle(row) }}</strong>
<span class="table-subtext">{{ row.server_region }} / {{ row.login_platform }}</span>
<div v-if="listingBadgeTags(row).length" class="admin-tag-row compact">
<el-tag
v-for="tag in listingBadgeTags(row)"
:key="`${row.id}-${tag}`"
size="small"
:type="tag === '特惠' ? 'danger' : 'warning'"
effect="plain"
>
{{ tag }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="号主" min-width="150">
<template #default="{ row }">
<strong>{{ ownerName(row) }}</strong>
<span class="table-subtext">ID: {{ row.owner_id }}</span>
</template>
</el-table-column>
<el-table-column label="资产配置" min-width="260">
<template #default="{ row }">
<div class="admin-tag-row">
<el-tag v-for="tag in listingAssetTags(row).slice(0, 8)" :key="`${row.id}-${tag}`" size="small" effect="plain">
{{ tag }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="资源" min-width="220">
<template #default="{ row }">
<div v-if="listingResourceTags(row).length" class="admin-tag-row">
<el-tag v-for="tag in listingResourceTags(row)" :key="`${row.id}-${tag}`" size="small" type="info" effect="plain">
{{ tag }}
</el-tag>
</div>
<span v-else class="table-subtext">无额外资源</span>
</template>
</el-table-column>
<el-table-column label="价格" min-width="170">
<template #default="{ row }">
<strong>{{ listingBuyerPrice(row) }}</strong>
<span class="table-subtext">卖家 {{ listingSellerPrice(row) }}</span>
<span class="table-subtext">
卖家 {{ formatRatio(getAdminListingPricing(row).sellerRatio) }} / 买家 {{ formatRatio(getAdminListingPricing(row).buyerRatio) }}
</span>
</template>
</el-table-column>
<el-table-column label="押金" width="100">
<template #default="{ row }">{{ formatMoney(row.deposit_amount) }}</template>
</el-table-column>
<el-table-column prop="deposit_amount" label="押金" width="90" />
<el-table-column label="截图" width="90">
<template #default="{ row }">
<el-button size="small" :disabled="!row.screenshot_urls?.length" @click="openEvidence(row)">
@@ -135,7 +187,9 @@ function readError(error: unknown, fallback: string) {
<el-dialog :model-value="!!activeListing" title="拒绝发布" width="560px" @update:model-value="activeListing = null">
<div v-if="activeListing" class="dialog-body">
<p><strong>{{ activeListing.title }}</strong></p>
<p><strong>{{ listingDisplayTitle(activeListing) }}</strong></p>
<p>买家日租{{ listingBuyerPrice(activeListing) }}卖家价格{{ listingSellerPrice(activeListing) }}</p>
<p>押金{{ formatMoney(activeListing.deposit_amount) }}</p>
<el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="填写拒绝原因,号主会在通知中看到审核结果" />
</div>
<template #footer>
@@ -146,7 +200,7 @@ function readError(error: unknown, fallback: string) {
<el-dialog :model-value="!!evidenceListing" title="账号资产截图" width="680px" @update:model-value="evidenceListing = null">
<div v-if="evidenceListing" class="dialog-body">
<p><strong>{{ evidenceListing.title }}</strong></p>
<p><strong>{{ listingDisplayTitle(evidenceListing) }}</strong></p>
<div v-if="evidenceListing.screenshot_urls?.length" class="evidence-list">
<div v-for="url in evidenceListing.screenshot_urls" :key="url" class="evidence-row">
<span>{{ url }}</span>
+67 -21
View File
@@ -3,6 +3,19 @@ import { Search } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { fetchAdminListings, type Listing } from '@/api/listings'
import {
formatMoney,
formatRatio,
getAdminListingPricing,
listingAssetTags,
listingBadgeTags,
listingBuyerPrice,
listingDisplayTitle,
listingResourceTags,
listingRiskTags,
listingSellerPrice,
ownerName,
} from '@/views/admin/listingAdminDisplay'
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
@@ -38,18 +51,6 @@ function resetFilters() {
void loadListings()
}
function money(value: number) {
return `¥${Number(value || 0).toFixed(2)}`
}
function listingPrice(row: Listing) {
return money(row.price_daily || row.price_hourly)
}
function ownerName(row: Listing) {
return row.owner_phone || row.owner_nickname || `号主 ${row.owner_id}`
}
function statusType(status: string) {
if (status === 'published') return 'success'
if (status === 'rented') return 'warning'
@@ -126,10 +127,21 @@ function reviewType(status: string) {
<el-table v-loading="loading" class="table-panel" :data="listings">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="商品" min-width="220">
<el-table-column label="商品" min-width="270" fixed="left">
<template #default="{ row }">
<strong>{{ row.title }}</strong>
<strong>{{ listingDisplayTitle(row) }}</strong>
<span class="table-subtext">账号 ID: {{ row.account_id }}</span>
<div v-if="listingBadgeTags(row).length" class="admin-tag-row compact">
<el-tag
v-for="tag in listingBadgeTags(row)"
:key="`${row.id}-${tag}`"
size="small"
:type="tag === '特惠' ? 'danger' : 'warning'"
effect="plain"
>
{{ tag }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="号主" min-width="150">
@@ -138,15 +150,49 @@ function reviewType(status: string) {
<span class="table-subtext">ID: {{ row.owner_id }}</span>
</template>
</el-table-column>
<el-table-column prop="server_region" label="区服" width="120" />
<el-table-column prop="login_platform" label="平台" width="120" />
<el-table-column prop="rank_level" label="段位" width="110" />
<el-table-column prop="haf_coin_amount" label="哈夫币" width="110" />
<el-table-column label="价格" width="100">
<template #default="{ row }">{{ listingPrice(row) }}</template>
<el-table-column label="资产配置" min-width="260">
<template #default="{ row }">
<div class="admin-tag-row">
<el-tag v-for="tag in listingAssetTags(row).slice(0, 8)" :key="`${row.id}-${tag}`" size="small" effect="plain">
{{ tag }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="资源" min-width="220">
<template #default="{ row }">
<div v-if="listingResourceTags(row).length" class="admin-tag-row">
<el-tag v-for="tag in listingResourceTags(row)" :key="`${row.id}-${tag}`" size="small" type="info" effect="plain">
{{ tag }}
</el-tag>
</div>
<span v-else class="table-subtext">无额外资源</span>
</template>
</el-table-column>
<el-table-column label="交接风控" min-width="230">
<template #default="{ row }">
<div class="admin-tag-row">
<el-tag v-for="tag in listingRiskTags(row)" :key="`${row.id}-${tag}`" size="small" type="info" effect="plain">
{{ tag }}
</el-tag>
</div>
<span class="table-subtext">{{ row.server_region }} / {{ row.login_platform }}</span>
</template>
</el-table-column>
<el-table-column label="价格" min-width="170">
<template #default="{ row }">
<strong>{{ listingBuyerPrice(row) }}</strong>
<span class="table-subtext">卖家 {{ listingSellerPrice(row) }}</span>
<span class="table-subtext">
卖家 {{ formatRatio(getAdminListingPricing(row).sellerRatio) }} / 买家 {{ formatRatio(getAdminListingPricing(row).buyerRatio) }}
</span>
<span v-if="getAdminListingPricing(row).platformMarkup > 0" class="table-subtext">
平台加价 {{ formatMoney(getAdminListingPricing(row).platformMarkup) }}
</span>
</template>
</el-table-column>
<el-table-column label="押金" width="100">
<template #default="{ row }">{{ money(row.deposit_amount) }}</template>
<template #default="{ row }">{{ formatMoney(row.deposit_amount) }}</template>
</el-table-column>
<el-table-column label="商品状态" width="110">
<template #default="{ row }">
@@ -0,0 +1,124 @@
import type { Listing } from '@/api/listings'
import {
assetRegions,
formatHafCoinM,
getCoinWan,
getListingResources,
getListingTitle,
getOnlineTimeText,
hasAcceleratedSaleRatio,
hasGiftResources,
readAssetNumber,
readAssetString,
} from '@/views/mobile/listingDisplay'
export interface AdminListingPricing {
buyerDaily: number
sellerDaily: number
sellerRatio: number
buyerRatio: number
platformMarkup: number
}
export function ownerName(row: Listing) {
return row.owner_phone || row.owner_nickname || `号主 ${row.owner_id}`
}
export function listingDisplayTitle(row: Listing) {
return getListingTitle(row) || row.title
}
export function formatMoney(value: number) {
return `¥${Number(value || 0).toFixed(2)}`
}
export function getAdminListingPricing(row: Listing): AdminListingPricing {
const breakdown = readBreakdown(row)
const buyerDaily = readBreakdownNumber(breakdown, 'buyer_total_price') || Number(row.price_daily || row.price_hourly || 0)
const sellerDaily = readBreakdownNumber(breakdown, 'seller_total_price') || buyerDaily
return {
buyerDaily,
sellerDaily,
sellerRatio: readBreakdownNumber(breakdown, 'seller_ratio'),
buyerRatio: readBreakdownNumber(breakdown, 'buyer_ratio') || readAssetNumber(row, 'publish_ratio'),
platformMarkup: readBreakdownNumber(breakdown, 'platform_markup_amount'),
}
}
export function listingBuyerPrice(row: Listing) {
return formatMoney(getAdminListingPricing(row).buyerDaily)
}
export function listingSellerPrice(row: Listing) {
return formatMoney(getAdminListingPricing(row).sellerDaily)
}
export function formatRatio(value: number) {
if (!Number.isFinite(value) || value <= 0) return '-'
const rounded = Math.round(value * 100) / 100
return `1:${Number.isInteger(rounded) ? rounded : rounded.toFixed(2)}`
}
export function listingAssetTags(row: Listing) {
return [
`哈夫币:${formatHafCoinM(getCoinWan(row))}`,
tagValue('保险', readAssetString(row, 'season_insurance')),
tagValue('体力', readAssetString(row, 'stamina_level')),
tagValue('负重', readAssetString(row, 'load_level')),
tagValue('段位', row.rank_level),
tagValue('日损', formatDailyLoss(row)),
tagValue('烽火', readAssetNumber(row, 'fire_level') || ''),
tagValue('KD', readAssetString(row, 'secret_kd')),
].filter(Boolean)
}
export function listingRiskTags(row: Listing) {
return [
tagValue('人脸', readAssetString(row, 'face_owner')),
tagValue('封禁', readAssetString(row, 'ban_record')),
tagValue('地区', assetRegions(row).join('/')),
tagValue('在线', getOnlineTimeText(row)),
].filter(Boolean)
}
export function listingResourceTags(row: Listing, limit = 4) {
const resources = getListingResources(row).map((resource) => {
const mode = resource.mode === '赠送' ? '赠送' : '收费'
return `${resource.label}:${resource.quantity}${mode === '赠送' ? '(赠)' : ''}`
})
if (resources.length <= limit) return resources
return [...resources.slice(0, limit), `+${resources.length - limit}`]
}
export function listingBadgeTags(row: Listing) {
return [
hasAcceleratedSaleRatio(row) ? '特惠' : '',
hasGiftResources(row) ? '有赠送' : '',
].filter(Boolean)
}
function formatDailyLoss(row: Listing) {
const value = readAssetNumber(row, 'daily_loss_m')
return value > 0 ? `${value}M/天` : ''
}
function tagValue(label: string, value: string | number) {
const text = String(value || '').trim()
return text ? `${label}:${text}` : ''
}
function readBreakdown(row: Listing) {
const breakdown = row.asset_summary?.price_breakdown
return typeof breakdown === 'object' && breakdown !== null ? (breakdown as Record<string, unknown>) : null
}
function readBreakdownNumber(row: Record<string, unknown> | null, key: string) {
if (!row) return 0
const value = row[key]
if (typeof value === 'number') return Number.isFinite(value) ? value : 0
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
return 0
}