feat(dashboard): 增加极品专区与保险成交统计

- 仪表盘新增今日极品专区、保险8%、保险4%成交数量
- 展示近7日每日成交趋势和明细
- 按已支付订单口径统计,补充回归测试
This commit is contained in:
yml2213
2026-11-06 19:09:35 +08:00
parent ee08e343f4
commit 87d252bbee
6 changed files with 382 additions and 0 deletions
@@ -42,6 +42,20 @@ export interface ListingDailyOverview {
timezone: string
}
export interface TradingDayStats {
date: string
premium_order_count: number
insurance_8_order_count: number
insurance_4_order_count: number
}
export interface TradingDailyOverview {
today: TradingDayStats
trend: TradingDayStats[]
days: number
timezone: string
}
export interface ListingUploaderDayStats {
date: string
uploader_id: number
@@ -82,6 +96,7 @@ export interface AdminDashboard {
metrics: DashboardMetrics
pending: DashboardPending
listing_daily: ListingDailyOverview
trading_daily: TradingDailyOverview
manual_entry_today: ManualEntryTodayStats[]
recent_orders: DashboardRecentOrder[]
recent_disputes: DashboardRecentDispute[]
@@ -95,10 +110,18 @@ const emptyDayStats = (): ListingDayStats => ({
trade_leave_count: 0,
})
const emptyTradingDayStats = (): TradingDayStats => ({
date: '',
premium_order_count: 0,
insurance_8_order_count: 0,
insurance_4_order_count: 0,
})
export async function fetchAdminDashboard() {
const { data } = await apiClient.get<ApiResponse<AdminDashboard>>('/admin/dashboard')
const payload = data.data
const listingDaily = payload.listing_daily
const tradingDaily = payload.trading_daily
return {
...payload,
listing_daily: {
@@ -111,6 +134,12 @@ export async function fetchAdminDashboard() {
days: listingDaily?.days ?? 7,
timezone: listingDaily?.timezone ?? 'Asia/Shanghai',
},
trading_daily: {
today: tradingDaily?.today ?? emptyTradingDayStats(),
trend: tradingDaily?.trend ?? [],
days: tradingDaily?.days ?? 7,
timezone: tradingDaily?.timezone ?? 'Asia/Shanghai',
},
manual_entry_today: payload.manual_entry_today ?? [],
recent_orders: payload.recent_orders ?? [],
recent_disputes: payload.recent_disputes ?? [],
@@ -335,6 +335,21 @@
gap: 12px;
}
.tone-premium .listing-stat-icon {
background: #fef3c7;
color: #d97706;
}
.tone-insurance8 .listing-stat-icon {
background: #ecfeff;
color: #0891b2;
}
.tone-insurance4 .listing-stat-icon {
background: #eff6ff;
color: #2563eb;
}
.listing-stat-icon {
width: 40px;
height: 40px;
@@ -422,6 +437,18 @@
background: linear-gradient(180deg, #60a5fa, #3b82f6);
}
.mini-bar.premium {
background: linear-gradient(180deg, #fcd34d, #f59e0b);
}
.mini-bar.insurance8 {
background: linear-gradient(180deg, #67e8f9, #0891b2);
}
.mini-bar.insurance4 {
background: linear-gradient(180deg, #93c5fd, #2563eb);
}
.mini-bar-col em {
font-style: normal;
font-size: 10px;
@@ -8,13 +8,17 @@ import {
Document,
Grid,
Loading,
Medal,
Operation,
Present,
Refresh,
ScaleToOriginal,
ShoppingBag,
Shop,
Tickets,
Top,
Trophy,
Umbrella,
User,
Van,
Wallet,
@@ -27,6 +31,7 @@ import {
type ListingDayStats,
type ListingUploaderDayStats,
type ManualEntryTodayStats,
type TradingDayStats,
} from '@/features/admin/api/adminDashboard'
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
import { formatCentWithSymbol } from '@/shared/utils/money'
@@ -43,6 +48,18 @@ const {
fetchFn: fetchAdminDashboard,
})
const tradingTrend = computed(() => dashboard.value?.trading_daily?.trend ?? [])
const tradingToday = computed<TradingDayStats>(
() =>
dashboard.value?.trading_daily?.today ?? {
date: '',
premium_order_count: 0,
insurance_8_order_count: 0,
insurance_4_order_count: 0,
}
)
const tradingDays = computed(() => dashboard.value?.trading_daily?.days ?? 7)
const listingTrend = computed(() => dashboard.value?.listing_daily?.trend ?? [])
const listingChannelTrend = computed(() => dashboard.value?.listing_daily?.channel_trend ?? [])
const listingUploaderTrend = computed(() => dashboard.value?.listing_daily?.uploader_trend ?? [])
@@ -94,6 +111,11 @@ function trendMax(getter: (row: ListingDayStats) => number) {
return Math.max(1, ...values, 0)
}
function tradingTrendMax(getter: (row: TradingDayStats) => number) {
const values = tradingTrend.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))}%`
@@ -194,6 +216,162 @@ function shortDate(date: string) {
</div>
</section>
<!-- 经营成交:极品专区 + 保险 8%/4% -->
<section v-if="dashboard" class="section-card">
<div class="section-header-row">
<div class="section-header-title">
<el-icon class="header-icon"><Trophy /></el-icon>
<span>经营成交统计</span>
<span class="header-badge">按北京时间自然日 · 近 {{ tradingDays }} 日</span>
</div>
</div>
<div class="listing-daily-grid">
<div class="listing-stat-card tone-premium">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Medal /></el-icon>
</div>
<div class="listing-stat-meta">
<span>今日极品专区成交</span>
<strong>{{ tradingToday.premium_order_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">已支付订单,按订单创建日归集(不含待支付/取消/关闭)</p>
<div class="mini-bars">
<div
v-for="row in tradingTrend"
:key="`premium-${row.date}`"
class="mini-bar-col"
:title="`${row.date}:${row.premium_order_count} 单`"
>
<div
class="mini-bar premium"
:style="{
height: barHeight(
row.premium_order_count,
tradingTrendMax(r => r.premium_order_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.premium_order_count }}</b>
</div>
</div>
</div>
<div class="listing-stat-card tone-insurance8">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Umbrella /></el-icon>
</div>
<div class="listing-stat-meta">
<span>今日保险 8% 成交</span>
<strong>{{ tradingToday.insurance_8_order_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">选择无忧保险 8% 的已支付订单</p>
<div class="mini-bars">
<div
v-for="row in tradingTrend"
:key="`ins8-${row.date}`"
class="mini-bar-col"
:title="`${row.date}:${row.insurance_8_order_count} 单`"
>
<div
class="mini-bar insurance8"
:style="{
height: barHeight(
row.insurance_8_order_count,
tradingTrendMax(r => r.insurance_8_order_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.insurance_8_order_count }}</b>
</div>
</div>
</div>
<div class="listing-stat-card tone-insurance4">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Present /></el-icon>
</div>
<div class="listing-stat-meta">
<span>今日保险 4% 成交</span>
<strong>{{ tradingToday.insurance_4_order_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">选择无忧保险 4% 的已支付订单</p>
<div class="mini-bars">
<div
v-for="row in tradingTrend"
:key="`ins4-${row.date}`"
class="mini-bar-col"
:title="`${row.date}:${row.insurance_4_order_count} 单`"
>
<div
class="mini-bar insurance4"
:style="{
height: barHeight(
row.insurance_4_order_count,
tradingTrendMax(r => r.insurance_4_order_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.insurance_4_order_count }}</b>
</div>
</div>
</div>
</div>
<div class="channel-summary-title">近 {{ tradingDays }} 日经营成交明细</div>
<div class="table-container">
<el-table class="custom-enterprise-table" :data="tradingTrend" size="small" stripe>
<el-table-column prop="date" label="统计日期" min-width="120" />
<el-table-column
prop="premium_order_count"
label="极品专区成交"
width="140"
align="center"
>
<template #default="{ row }">
<span class="trend-val text-emerald">{{ row.premium_order_count }}</span>
</template>
</el-table-column>
<el-table-column
prop="insurance_8_order_count"
label="保险 8%"
width="120"
align="center"
>
<template #default="{ row }">
<span class="trend-val text-amber">{{ row.insurance_8_order_count }}</span>
</template>
</el-table-column>
<el-table-column
prop="insurance_4_order_count"
label="保险 4%"
width="120"
align="center"
>
<template #default="{ row }">
<span class="trend-val text-blue">{{ row.insurance_4_order_count }}</span>
</template>
</el-table-column>
<el-table-column label="保险合计" width="120" align="center">
<template #default="{ row }">
<strong class="trend-val text-slate">{{
row.insurance_8_order_count + row.insurance_4_order_count
}}</strong>
</template>
</el-table-column>
</el-table>
</div>
</section>
<!-- 账号上下架:今日三组 + 近 7 日趋势 -->
<section v-if="dashboard" class="section-card">
<div class="section-header-row">