feat(dashboard): 增加极品专区与保险成交统计
- 仪表盘新增今日极品专区、保险8%、保险4%成交数量 - 展示近7日每日成交趋势和明细 - 按已支付订单口径统计,补充回归测试
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user