增加渠道上传预统计

This commit is contained in:
yml2213
2026-07-25 14:34:03 +08:00
parent 1ea01f7d46
commit 7ca68e6b8b
15 changed files with 689 additions and 63 deletions
@@ -27,9 +27,15 @@ export interface ListingDayStats {
trade_leave_count: number
}
export interface ListingChannelDayStats extends ListingDayStats {
source_channel: string
}
export interface ListingDailyOverview {
today: ListingDayStats
trend: ListingDayStats[]
today_channels: ListingChannelDayStats[]
channel_trend: ListingChannelDayStats[]
days: number
timezone: string
}
@@ -82,6 +88,8 @@ export async function fetchAdminDashboard() {
listing_daily: {
today: listingDaily?.today ?? emptyDayStats(),
trend: listingDaily?.trend ?? [],
today_channels: listingDaily?.today_channels ?? [],
channel_trend: listingDaily?.channel_trend ?? [],
days: listingDaily?.days ?? 7,
timezone: listingDaily?.timezone ?? 'Asia/Shanghai',
},
@@ -22,6 +22,7 @@ import {
import {
fetchAdminDashboard,
type AdminDashboard,
type ListingChannelDayStats,
type ListingDayStats,
} from '@/features/admin/api/adminDashboard'
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
@@ -40,6 +41,7 @@ const {
})
const listingTrend = computed(() => dashboard.value?.listing_daily?.trend ?? [])
const listingChannelTrend = computed(() => dashboard.value?.listing_daily?.channel_trend ?? [])
const listingToday = computed(
() =>
dashboard.value?.listing_daily?.today ?? {
@@ -49,6 +51,25 @@ const listingToday = computed(
trade_leave_count: 0,
}
)
const channelDisplayOrder = ['咸鱼', '淘宝', '京东', 'QQ', '微信', '自定义', '站内发布', '未填写']
const listingTodayChannels = computed<ListingChannelDayStats[]>(() => {
const date = listingToday.value.date
const rows = dashboard.value?.listing_daily?.today_channels ?? []
const byChannel = new Map(rows.map(row => [row.source_channel || '未填写', row]))
const ordered = channelDisplayOrder.map(channel => ({
date,
source_channel: channel,
published_count: byChannel.get(channel)?.published_count ?? 0,
active_offline_count: byChannel.get(channel)?.active_offline_count ?? 0,
trade_leave_count: byChannel.get(channel)?.trade_leave_count ?? 0,
}))
const extras = rows.filter(row => !channelDisplayOrder.includes(row.source_channel || '未填写'))
return [...ordered, ...extras]
})
function leaveCount(row: ListingChannelDayStats | ListingDayStats) {
return Number(row.active_offline_count || 0) + Number(row.trade_leave_count || 0)
}
function trendMax(getter: (row: ListingDayStats) => number) {
const values = listingTrend.value.map(getter)
@@ -248,6 +269,24 @@ function shortDate(date: string) {
</div>
</div>
<div class="listing-channel-summary">
<div
v-for="row in listingTodayChannels"
:key="`today-channel-${row.source_channel}`"
class="listing-channel-item"
>
<div class="listing-channel-item-head">
<span>{{ row.source_channel || '未填写' }}</span>
<strong>{{ row.published_count + leaveCount(row) }}</strong>
</div>
<div class="listing-channel-counts">
<span>上架 {{ row.published_count }}</span>
<span>主动下架 {{ row.active_offline_count }}</span>
<span>交易离架 {{ row.trade_leave_count }}</span>
</div>
</div>
</div>
<el-table class="table-panel listing-trend-table" :data="listingTrend" size="small">
<el-table-column prop="date" label="日期" min-width="120" />
<el-table-column prop="published_count" label="上架" width="100" align="center" />
@@ -259,6 +298,35 @@ function shortDate(date: string) {
</template>
</el-table-column>
</el-table>
<div v-if="listingChannelTrend.length" class="listing-channel-block">
<div class="listing-channel-title"> 7 日渠道明细</div>
<el-table
class="table-panel listing-channel-table"
:data="listingChannelTrend"
size="small"
>
<el-table-column prop="date" label="日期" min-width="120" />
<el-table-column prop="source_channel" label="渠道" min-width="120">
<template #default="{ row }">
<span class="listing-channel-name">{{ row.source_channel || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="published_count" label="上架" width="100" align="center" />
<el-table-column
prop="active_offline_count"
label="主动下架"
width="110"
align="center"
/>
<el-table-column prop="trade_leave_count" label="交易离架" width="110" align="center" />
<el-table-column label="合计离架" width="100" align="center">
<template #default="{ row }">
{{ leaveCount(row) }}
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 待处理事项 & 快捷入口 -->
@@ -789,10 +857,80 @@ function shortDate(date: string) {
margin-top: 4px;
}
.listing-channel-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin: 0 0 16px;
}
.listing-channel-item {
min-width: 0;
border: 1px solid #e8edf4;
border-radius: 8px;
background: #ffffff;
padding: 12px;
}
.listing-channel-item-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.listing-channel-item-head span {
min-width: 0;
color: #1b2559;
font-size: 13px;
font-weight: 800;
overflow-wrap: anywhere;
}
.listing-channel-item-head strong {
flex-shrink: 0;
color: #4f7cff;
font-size: 20px;
font-weight: 900;
line-height: 1;
}
.listing-channel-counts {
display: grid;
gap: 5px;
margin-top: 10px;
}
.listing-channel-counts span {
color: #8f9bba;
font-size: 12px;
font-weight: 700;
}
.listing-channel-block {
margin-top: 16px;
}
.listing-channel-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 700;
color: #1b2559;
}
.listing-channel-name {
font-weight: 700;
color: #1b2559;
}
@media (max-width: 1100px) {
.listing-daily-grid {
grid-template-columns: 1fr;
}
.listing-channel-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
@@ -802,6 +940,10 @@ function shortDate(date: string) {
}
@media (max-width: 480px) {
.listing-channel-summary {
grid-template-columns: 1fr;
}
.metric-card {
flex-direction: column;
align-items: flex-start;
@@ -137,7 +137,12 @@ function selectListing(row: Listing) {
function replaceListing(next: Listing) {
const index = listings.value.findIndex(item => item.id === next.id)
if (index >= 0) {
listings.value.splice(index, 1, next)
const previous = listings.value[index]!
listings.value.splice(index, 1, {
...previous,
...next,
source_channel: next.source_channel || previous.source_channel,
})
} else {
listings.value.unshift(next)
}
@@ -422,6 +427,13 @@ function contactPhone(row: Listing) {
return typeof value === 'string' && value.trim() ? value : '-'
}
function sourceChannel(row: Listing) {
if (typeof row.source_channel === 'string' && row.source_channel.trim()) {
return row.source_channel.trim()
}
return isExternalUpload(row) ? '未填写' : '站内发布'
}
function ownerOnlineText(row: Listing) {
const text = row.asset_summary?.online_time_text
if (typeof text === 'string' && text.trim()) return text
@@ -456,7 +468,15 @@ function isDefaultScreenshot(url: string) {
function riskItems(row: Listing): RiskItem[] {
const items: RiskItem[] = []
if (isExternalUpload(row)) items.push({ label: `外部上传:${uploaderName(row)}`, level: 'info' })
if (isExternalUpload(row)) {
items.push({
label: `外部上传:${uploaderName(row)} / 渠道:${sourceChannel(row)}`,
level: 'info',
})
} else {
items.push({ label: `来源渠道:${sourceChannel(row)}`, level: 'info' })
}
const infoCount = items.length
if (!row.screenshot_urls?.length) items.push({ label: '没有账号截图', level: 'danger' })
if (hasDefaultScreenshot(row)) items.push({ label: '使用默认截图', level: 'warning' })
if (hasBanRecord(row)) items.push({ label: `封禁记录:${banRecordText(row)}`, level: 'danger' })
@@ -472,7 +492,7 @@ function riskItems(row: Listing): RiskItem[] {
items.push({ label: '烽火等级接近下限', level: 'warning' })
if (!readAssetString(row, 'season_insurance'))
items.push({ label: '缺少保险格数', level: 'warning' })
if (!items.length) items.push({ label: '未发现明显风险', level: 'info' })
if (items.length === infoCount) items.push({ label: '未发现明显风险', level: 'info' })
return items
}
@@ -495,6 +515,7 @@ function reviewSearchText(row: Listing) {
row.login_platform,
row.rank_level,
uploaderName(row),
sourceChannel(row),
getSkinNames(row).join(' '),
]
.join(' ')
@@ -602,7 +623,7 @@ async function openScreenshot(url: string) {
v-model="filters.keyword"
:prefix-icon="Search"
clearable
placeholder="搜索编号、标题、客服、段位、皮肤"
placeholder="搜索编号、标题、客服、渠道、段位、皮肤"
/>
<el-select v-model="filters.risk" placeholder="风险筛选">
<el-option label="全部待审" value="all" />
@@ -642,10 +663,17 @@ async function openScreenshot(url: string) {
formatCent(item.deposit_amount_cent)
}}</span
>
<el-tag v-if="isExternalUpload(item)" size="small" type="info">{{
uploaderName(item)
}}</el-tag>
<el-tag v-if="hasDefaultScreenshot(item)" size="small" type="warning">默认图</el-tag>
<div class="queue-tags">
<el-tag size="small" type="primary" effect="plain">{{
sourceChannel(item)
}}</el-tag>
<el-tag v-if="isExternalUpload(item)" size="small" type="info">{{
uploaderName(item)
}}</el-tag>
<el-tag v-if="hasDefaultScreenshot(item)" size="small" type="warning"
>默认图</el-tag
>
</div>
</div>
</button>
<el-empty
@@ -782,11 +810,20 @@ async function openScreenshot(url: string) {
<div class="review-panel">
<div class="panel-title">
<h3>上传信息</h3>
<el-tag v-if="isExternalUpload(selectedListing)" size="small" type="info"
>外部上传</el-tag
>
<div class="panel-tags">
<el-tag size="small" type="primary" effect="plain">{{
sourceChannel(selectedListing)
}}</el-tag>
<el-tag v-if="isExternalUpload(selectedListing)" size="small" type="info"
>外部上传</el-tag
>
</div>
</div>
<dl class="detail-list">
<div>
<dt>来源渠道</dt>
<dd>{{ sourceChannel(selectedListing) }}</dd>
</div>
<div>
<dt>上传人</dt>
<dd>{{ uploaderName(selectedListing) }}</dd>
@@ -1206,6 +1243,14 @@ async function openScreenshot(url: string) {
align-items: center;
}
.queue-tags,
.panel-tags {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.review-detail {
display: flex;
flex-direction: column;
@@ -9,6 +9,7 @@ export interface Listing {
owner_id: number
owner_phone?: string
owner_nickname?: string
source_channel?: string
title: string
description: string
game_name: string