账号上下架增加统计

This commit is contained in:
yml2213
2026-07-11 23:37:24 +08:00
parent 7614c0980e
commit 42b250cd2c
17 changed files with 792 additions and 27 deletions
@@ -20,6 +20,20 @@ export interface DashboardPending {
pending_return_confirms: number
}
export interface ListingDayStats {
date: string
published_count: number
active_offline_count: number
trade_leave_count: number
}
export interface ListingDailyOverview {
today: ListingDayStats
trend: ListingDayStats[]
days: number
timezone: string
}
export interface DashboardRecentOrder {
id: number
order_no: string
@@ -46,16 +60,32 @@ export interface DashboardRecentDispute {
export interface AdminDashboard {
metrics: DashboardMetrics
pending: DashboardPending
listing_daily: ListingDailyOverview
recent_orders: DashboardRecentOrder[]
recent_disputes: DashboardRecentDispute[]
generated_at: string
}
const emptyDayStats = (): ListingDayStats => ({
date: '',
published_count: 0,
active_offline_count: 0,
trade_leave_count: 0,
})
export async function fetchAdminDashboard() {
const { data } = await apiClient.get<ApiResponse<AdminDashboard>>('/admin/dashboard')
const payload = data.data
const listingDaily = payload.listing_daily
return {
...data.data,
recent_orders: data.data.recent_orders ?? [],
recent_disputes: data.data.recent_disputes ?? [],
...payload,
listing_daily: {
today: listingDaily?.today ?? emptyDayStats(),
trend: listingDaily?.trend ?? [],
days: listingDaily?.days ?? 7,
timezone: listingDaily?.timezone ?? 'Asia/Shanghai',
},
recent_orders: payload.recent_orders ?? [],
recent_disputes: payload.recent_disputes ?? [],
}
}
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue'
import {
Bell,
Bottom,
ChatDotRound,
Document,
Grid,
@@ -11,11 +13,17 @@ import {
ShoppingBag,
Shop,
Tickets,
Top,
User,
Van,
Wallet,
Warning,
} from '@element-plus/icons-vue'
import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard'
import {
fetchAdminDashboard,
type AdminDashboard,
type ListingDayStats,
} from '@/features/admin/api/adminDashboard'
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { adminPath } from '@/shared/utils/adminPath'
@@ -30,6 +38,32 @@ const {
} = useAdminTable<AdminDashboard>({
fetchFn: fetchAdminDashboard,
})
const listingTrend = computed(() => dashboard.value?.listing_daily?.trend ?? [])
const listingToday = computed(
() =>
dashboard.value?.listing_daily?.today ?? {
date: '',
published_count: 0,
active_offline_count: 0,
trade_leave_count: 0,
}
)
function trendMax(getter: (row: ListingDayStats) => number) {
const values = listingTrend.value.map(getter)
return Math.max(1, ...values, 0)
}
function barHeight(value: number, max: number) {
if (max <= 0) return '8%'
return `${Math.max(8, Math.round((value / max) * 100))}%`
}
function shortDate(date: string) {
if (!date || date.length < 10) return date
return date.slice(5)
}
</script>
<template>
@@ -103,6 +137,122 @@ const {
</div>
</div>
<!-- 账号上下架今日三组 + 7 日趋势 -->
<div v-if="dashboard" class="dashboard-section listing-daily-section">
<h2 class="section-title">
<el-icon><Shop /></el-icon>
账号上下架
<small class="section-subtitle">按北京时间自然日 · {{ dashboard.listing_daily.days }} </small>
</h2>
<div class="listing-daily-grid">
<div class="listing-stat-card tone-publish">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Top /></el-icon>
</div>
<div>
<span>今日上架</span>
<strong>{{ listingToday.published_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">免审发布 / 审核通过 / 取消订单恢复上架</p>
<div class="mini-bars">
<div
v-for="row in listingTrend"
:key="`pub-${row.date}`"
class="mini-bar-col"
:title="`${row.date}${row.published_count}`"
>
<div
class="mini-bar publish"
:style="{
height: barHeight(row.published_count, trendMax(r => r.published_count)),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.published_count }}</b>
</div>
</div>
</div>
<div class="listing-stat-card tone-offline">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Bottom /></el-icon>
</div>
<div>
<span>今日主动下架</span>
<strong>{{ listingToday.active_offline_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">号主手动下架 + 后台下架</p>
<div class="mini-bars">
<div
v-for="row in listingTrend"
:key="`off-${row.date}`"
class="mini-bar-col"
:title="`${row.date}${row.active_offline_count}`"
>
<div
class="mini-bar offline"
:style="{
height: barHeight(
row.active_offline_count,
trendMax(r => r.active_offline_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.active_offline_count }}</b>
</div>
</div>
</div>
<div class="listing-stat-card tone-trade">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Van /></el-icon>
</div>
<div>
<span>今日交易离架</span>
<strong>{{ listingToday.trade_leave_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">租出 / 售出完成 / 封存 / 订单仲裁归档</p>
<div class="mini-bars">
<div
v-for="row in listingTrend"
:key="`trade-${row.date}`"
class="mini-bar-col"
:title="`${row.date}${row.trade_leave_count}`"
>
<div
class="mini-bar trade"
:style="{
height: barHeight(row.trade_leave_count, trendMax(r => r.trade_leave_count)),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.trade_leave_count }}</b>
</div>
</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" />
<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 }">
{{ row.active_offline_count + row.trade_leave_count }}
</template>
</el-table-column>
</el-table>
</div>
<!-- 待处理事项 & 快捷入口 -->
<div v-if="dashboard" class="dashboard-panels">
<div class="dashboard-panel">
@@ -500,6 +650,143 @@ const {
color: #1b2559;
}
.section-subtitle {
margin-left: 8px;
font-size: 12px;
font-weight: 500;
color: #a3aed0;
}
.listing-daily-section {
margin-top: 24px;
}
.listing-daily-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.listing-stat-card {
padding: 18px 18px 14px;
border-radius: 14px;
background: #fff;
border: 1px solid #eef1f8;
box-shadow: 0 4px 14px rgba(27, 37, 89, 0.04);
}
.listing-stat-head {
display: flex;
align-items: center;
gap: 12px;
}
.listing-stat-icon {
width: 42px;
height: 42px;
border-radius: 12px;
display: grid;
place-items: center;
font-size: 18px;
flex-shrink: 0;
}
.tone-publish .listing-stat-icon {
background: rgba(16, 185, 129, 0.12);
color: #10b981;
}
.tone-offline .listing-stat-icon {
background: rgba(245, 158, 11, 0.12);
color: #f59e0b;
}
.tone-trade .listing-stat-icon {
background: rgba(79, 124, 255, 0.12);
color: #4f7cff;
}
.listing-stat-head span {
display: block;
font-size: 13px;
color: #8f9bba;
}
.listing-stat-head strong {
display: block;
margin-top: 4px;
font-size: 28px;
font-weight: 800;
color: #1b2559;
line-height: 1;
}
.listing-stat-hint {
margin: 10px 0 14px;
font-size: 12px;
color: #a3aed0;
}
.mini-bars {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 6px;
align-items: end;
min-height: 96px;
}
.mini-bar-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
min-width: 0;
}
.mini-bar {
width: 100%;
max-width: 22px;
border-radius: 6px 6px 2px 2px;
min-height: 6px;
transition: height 0.2s ease;
}
.mini-bar.publish {
background: linear-gradient(180deg, #34d399, #10b981);
}
.mini-bar.offline {
background: linear-gradient(180deg, #fbbf24, #f59e0b);
}
.mini-bar.trade {
background: linear-gradient(180deg, #7aa2ff, #4f7cff);
}
.mini-bar-col em,
.mini-bar-col b {
font-style: normal;
font-size: 10px;
line-height: 1;
color: #a3aed0;
}
.mini-bar-col b {
color: #1b2559;
font-weight: 700;
}
.listing-trend-table {
margin-top: 4px;
}
@media (max-width: 1100px) {
.listing-daily-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.quick-links {
grid-template-columns: repeat(2, 1fr);