账号上下架增加统计
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user