feat(dashboard): 增加极品专区与保险成交统计
- 仪表盘新增今日极品专区、保险8%、保险4%成交数量 - 展示近7日每日成交趋势和明细 - 按已支付订单口径统计,补充回归测试
This commit is contained in:
@@ -6,6 +6,7 @@ type DashboardDTO struct {
|
|||||||
Metrics MetricsDTO `json:"metrics"`
|
Metrics MetricsDTO `json:"metrics"`
|
||||||
Pending PendingDTO `json:"pending"`
|
Pending PendingDTO `json:"pending"`
|
||||||
ListingDaily ListingDailyOverviewDTO `json:"listing_daily"`
|
ListingDaily ListingDailyOverviewDTO `json:"listing_daily"`
|
||||||
|
TradingDaily TradingDailyOverviewDTO `json:"trading_daily"`
|
||||||
ManualEntryToday []ManualEntryAgentStatsDTO `json:"manual_entry_today"`
|
ManualEntryToday []ManualEntryAgentStatsDTO `json:"manual_entry_today"`
|
||||||
RecentOrders []RecentOrderDTO `json:"recent_orders"`
|
RecentOrders []RecentOrderDTO `json:"recent_orders"`
|
||||||
RecentDisputes []RecentDisputeDTO `json:"recent_disputes"`
|
RecentDisputes []RecentDisputeDTO `json:"recent_disputes"`
|
||||||
@@ -57,6 +58,25 @@ type ListingUploaderDayStatsDTO struct {
|
|||||||
UploadCount int64 `json:"upload_count"`
|
UploadCount int64 `json:"upload_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TradingDailyOverviewDTO 经营成交统计:今日指标 + 近 7 日趋势。
|
||||||
|
// 统计口径:已支付订单(status 不在 pending_payment/cancelled/closed),按创建日归属北京时间自然日。
|
||||||
|
type TradingDailyOverviewDTO struct {
|
||||||
|
Today TradingDayStatsDTO `json:"today"`
|
||||||
|
Trend []TradingDayStatsDTO `json:"trend"`
|
||||||
|
Days int `json:"days"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TradingDayStatsDTO struct {
|
||||||
|
Date string `json:"date"` // YYYY-MM-DD(上海时区)
|
||||||
|
// PremiumOrderCount 极品专区成交订单数。
|
||||||
|
PremiumOrderCount int64 `json:"premium_order_count"`
|
||||||
|
// Insurance8OrderCount 选择保险 8% 的成交订单数。
|
||||||
|
Insurance8OrderCount int64 `json:"insurance_8_order_count"`
|
||||||
|
// Insurance4OrderCount 选择保险 4% 的成交订单数。
|
||||||
|
Insurance4OrderCount int64 `json:"insurance_4_order_count"`
|
||||||
|
}
|
||||||
|
|
||||||
// ManualEntryAgentStatsDTO 今日客服手动录单统计(按客服聚合)。
|
// ManualEntryAgentStatsDTO 今日客服手动录单统计(按客服聚合)。
|
||||||
type ManualEntryAgentStatsDTO struct {
|
type ManualEntryAgentStatsDTO struct {
|
||||||
AdminID uint64 `json:"admin_id"`
|
AdminID uint64 `json:"admin_id"`
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
listingDailyTrendDays = 7
|
listingDailyTrendDays = 7
|
||||||
|
tradingDailyTrendDays = 7
|
||||||
listingChannelWebsite = "站内发布"
|
listingChannelWebsite = "站内发布"
|
||||||
listingChannelExternalUnknown = "未填写"
|
listingChannelExternalUnknown = "未填写"
|
||||||
|
// 成交口径:排除待支付、取消、关闭,与推广归因的成交统计保持一致。
|
||||||
|
tradingOrderPaidCondition = "status NOT IN ('pending_payment', 'cancelled', 'closed')"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
@@ -78,6 +81,10 @@ func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
tradingDaily, err := r.tradingDailyOverview(ctx, now, tradingDailyTrendDays)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
manualEntryToday, err := r.manualEntryToday(ctx, now)
|
manualEntryToday, err := r.manualEntryToday(ctx, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -94,6 +101,7 @@ func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
|
|||||||
Metrics: metrics,
|
Metrics: metrics,
|
||||||
Pending: pending,
|
Pending: pending,
|
||||||
ListingDaily: listingDaily,
|
ListingDaily: listingDaily,
|
||||||
|
TradingDaily: tradingDaily,
|
||||||
ManualEntryToday: manualEntryToday,
|
ManualEntryToday: manualEntryToday,
|
||||||
RecentOrders: recentOrders,
|
RecentOrders: recentOrders,
|
||||||
RecentDisputes: recentDisputes,
|
RecentDisputes: recentDisputes,
|
||||||
@@ -353,6 +361,76 @@ func isTradeLeaveEvent(eventType, source string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tradingDailyOverview 统计经营成交数据:极品专区成交、保险 8%/4% 成交。
|
||||||
|
// 订单在下单时已快照 is_premium / compensation_plan / insurance_fee_bps,
|
||||||
|
// 因此按订单统计不受商品后续修改影响。
|
||||||
|
func (r *Repository) tradingDailyOverview(ctx context.Context, now time.Time, days int) (TradingDailyOverviewDTO, error) {
|
||||||
|
if days <= 0 {
|
||||||
|
days = tradingDailyTrendDays
|
||||||
|
}
|
||||||
|
loc := timeutil.ShanghaiLocation()
|
||||||
|
now = now.In(loc)
|
||||||
|
end := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, 1)
|
||||||
|
start := end.AddDate(0, 0, -days)
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
CreatedAt time.Time
|
||||||
|
IsPremium bool
|
||||||
|
CompensationPlan string
|
||||||
|
InsuranceFeeBps int
|
||||||
|
}
|
||||||
|
rows := make([]row, 0)
|
||||||
|
// 读取区间内订单后在 Go 侧按上海时区归日,避免依赖 MySQL 会话时区。
|
||||||
|
if err := r.db.WithContext(ctx).Table("rental_orders").
|
||||||
|
Select("created_at, is_premium, compensation_plan, insurance_fee_bps").
|
||||||
|
Where(tradingOrderPaidCondition).
|
||||||
|
Where("created_at >= ? AND created_at < ?", start, end).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return TradingDailyOverviewDTO{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
byDay := make(map[string]TradingDayStatsDTO, days)
|
||||||
|
for _, item := range rows {
|
||||||
|
key := item.CreatedAt.In(loc).Format("2006-01-02")
|
||||||
|
stats := byDay[key]
|
||||||
|
stats.Date = key
|
||||||
|
if item.IsPremium {
|
||||||
|
stats.PremiumOrderCount++
|
||||||
|
}
|
||||||
|
if item.CompensationPlan == "insurance" {
|
||||||
|
switch item.InsuranceFeeBps {
|
||||||
|
case 800:
|
||||||
|
stats.Insurance8OrderCount++
|
||||||
|
case 400:
|
||||||
|
stats.Insurance4OrderCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byDay[key] = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
trend := make([]TradingDayStatsDTO, 0, days)
|
||||||
|
for i := days - 1; i >= 0; i-- {
|
||||||
|
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -i)
|
||||||
|
key := day.Format("2006-01-02")
|
||||||
|
item := TradingDayStatsDTO{Date: key}
|
||||||
|
if v, ok := byDay[key]; ok {
|
||||||
|
item = v
|
||||||
|
}
|
||||||
|
trend = append(trend, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
today := TradingDayStatsDTO{Date: now.Format("2006-01-02")}
|
||||||
|
if len(trend) > 0 {
|
||||||
|
today = trend[len(trend)-1]
|
||||||
|
}
|
||||||
|
return TradingDailyOverviewDTO{
|
||||||
|
Today: today,
|
||||||
|
Trend: trend,
|
||||||
|
Days: days,
|
||||||
|
Timezone: "Asia/Shanghai",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// manualEntryToday 统计今日(北京时间自然日)各客服的手动录单数量。
|
// manualEntryToday 统计今日(北京时间自然日)各客服的手动录单数量。
|
||||||
// 录单表是后来新增的功能表,若迁移尚未执行(表不存在)则降级为空统计,
|
// 录单表是后来新增的功能表,若迁移尚未执行(表不存在)则降级为空统计,
|
||||||
// 避免连带整个仪表盘不可用。
|
// 避免连带整个仪表盘不可用。
|
||||||
|
|||||||
@@ -106,3 +106,53 @@ func TestListingDailyOverviewGroupsExternalUploadsByUploader(t *testing.T) {
|
|||||||
t.Fatalf("uploader trend = %#v, want 3 rows", overview.UploaderTrend)
|
t.Fatalf("uploader trend = %#v, want 3 rows", overview.UploaderTrend)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTradingDailyOverviewCountsPremiumAndInsurance(t *testing.T) {
|
||||||
|
db := database.NewTestDB()
|
||||||
|
if err := database.AutoMigrateIfMissing(db, &model.RentalOrder{}); err != nil {
|
||||||
|
t.Fatalf("AutoMigrate() error = %v", err)
|
||||||
|
}
|
||||||
|
loc := timeutil.ShanghaiLocation()
|
||||||
|
now := time.Date(2026, 7, 25, 12, 0, 0, 0, loc)
|
||||||
|
|
||||||
|
orders := []model.RentalOrder{
|
||||||
|
// 今日:极品 + 保险8%,成交
|
||||||
|
{OrderNo: "T1", ListingID: 1, AccountID: 1, OwnerID: 1, RenterID: 1, Status: "pending_handoff", IsPremium: true, CompensationPlan: "insurance", InsuranceFeeBps: 800, CreatedAt: now},
|
||||||
|
// 今日:极品 + 保险4%,成交
|
||||||
|
{OrderNo: "T2", ListingID: 2, AccountID: 2, OwnerID: 2, RenterID: 2, Status: "renting", IsPremium: true, CompensationPlan: "insurance", InsuranceFeeBps: 400, CreatedAt: now.Add(time.Hour)},
|
||||||
|
// 今日:非极品 + 保险4%,成交
|
||||||
|
{OrderNo: "T3", ListingID: 3, AccountID: 3, OwnerID: 3, RenterID: 3, Status: "completed", CompensationPlan: "insurance", InsuranceFeeBps: 400, CreatedAt: now.Add(2 * time.Hour)},
|
||||||
|
// 今日:待支付,不计入成交
|
||||||
|
{OrderNo: "T4", ListingID: 4, AccountID: 4, OwnerID: 4, RenterID: 4, Status: "pending_payment", IsPremium: true, CompensationPlan: "insurance", InsuranceFeeBps: 400, CreatedAt: now.Add(3 * time.Hour)},
|
||||||
|
// 今日:已取消,不计入成交
|
||||||
|
{OrderNo: "T5", ListingID: 5, AccountID: 5, OwnerID: 5, RenterID: 5, Status: "cancelled", IsPremium: true, CompensationPlan: "insurance", InsuranceFeeBps: 800, CreatedAt: now.Add(4 * time.Hour)},
|
||||||
|
// 昨日:极品 + 保险8%,成交
|
||||||
|
{OrderNo: "T6", ListingID: 6, AccountID: 6, OwnerID: 6, RenterID: 6, Status: "completed", IsPremium: true, CompensationPlan: "insurance", InsuranceFeeBps: 800, CreatedAt: now.AddDate(0, 0, -1)},
|
||||||
|
// 前日:普通赔付,不计入保险
|
||||||
|
{OrderNo: "T7", ListingID: 7, AccountID: 7, OwnerID: 7, RenterID: 7, Status: "completed", CompensationPlan: "normal", CreatedAt: now.AddDate(0, 0, -2)},
|
||||||
|
}
|
||||||
|
if err := db.Create(&orders).Error; err != nil {
|
||||||
|
t.Fatalf("create orders error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
overview, err := NewRepository(db).tradingDailyOverview(t.Context(), now, 7)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tradingDailyOverview() error = %v", err)
|
||||||
|
}
|
||||||
|
if overview.Days != 7 || len(overview.Trend) != 7 {
|
||||||
|
t.Fatalf("trend = %#v, want 7 days", overview.Trend)
|
||||||
|
}
|
||||||
|
if overview.Today.Date != "2026-07-25" {
|
||||||
|
t.Fatalf("today date = %q, want 2026-07-25", overview.Today.Date)
|
||||||
|
}
|
||||||
|
if overview.Today.PremiumOrderCount != 2 ||
|
||||||
|
overview.Today.Insurance8OrderCount != 1 ||
|
||||||
|
overview.Today.Insurance4OrderCount != 2 {
|
||||||
|
t.Fatalf("today stats = %#v, want premium=2 insurance8=1 insurance4=2", overview.Today)
|
||||||
|
}
|
||||||
|
|
||||||
|
yesterday := overview.Trend[len(overview.Trend)-2]
|
||||||
|
if yesterday.PremiumOrderCount != 1 || yesterday.Insurance8OrderCount != 1 || yesterday.Insurance4OrderCount != 0 {
|
||||||
|
t.Fatalf("yesterday stats = %#v, want premium=1 insurance8=1 insurance4=0", yesterday)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ export interface ListingDailyOverview {
|
|||||||
timezone: string
|
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 {
|
export interface ListingUploaderDayStats {
|
||||||
date: string
|
date: string
|
||||||
uploader_id: number
|
uploader_id: number
|
||||||
@@ -82,6 +96,7 @@ export interface AdminDashboard {
|
|||||||
metrics: DashboardMetrics
|
metrics: DashboardMetrics
|
||||||
pending: DashboardPending
|
pending: DashboardPending
|
||||||
listing_daily: ListingDailyOverview
|
listing_daily: ListingDailyOverview
|
||||||
|
trading_daily: TradingDailyOverview
|
||||||
manual_entry_today: ManualEntryTodayStats[]
|
manual_entry_today: ManualEntryTodayStats[]
|
||||||
recent_orders: DashboardRecentOrder[]
|
recent_orders: DashboardRecentOrder[]
|
||||||
recent_disputes: DashboardRecentDispute[]
|
recent_disputes: DashboardRecentDispute[]
|
||||||
@@ -95,10 +110,18 @@ const emptyDayStats = (): ListingDayStats => ({
|
|||||||
trade_leave_count: 0,
|
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() {
|
export async function fetchAdminDashboard() {
|
||||||
const { data } = await apiClient.get<ApiResponse<AdminDashboard>>('/admin/dashboard')
|
const { data } = await apiClient.get<ApiResponse<AdminDashboard>>('/admin/dashboard')
|
||||||
const payload = data.data
|
const payload = data.data
|
||||||
const listingDaily = payload.listing_daily
|
const listingDaily = payload.listing_daily
|
||||||
|
const tradingDaily = payload.trading_daily
|
||||||
return {
|
return {
|
||||||
...payload,
|
...payload,
|
||||||
listing_daily: {
|
listing_daily: {
|
||||||
@@ -111,6 +134,12 @@ export async function fetchAdminDashboard() {
|
|||||||
days: listingDaily?.days ?? 7,
|
days: listingDaily?.days ?? 7,
|
||||||
timezone: listingDaily?.timezone ?? 'Asia/Shanghai',
|
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 ?? [],
|
manual_entry_today: payload.manual_entry_today ?? [],
|
||||||
recent_orders: payload.recent_orders ?? [],
|
recent_orders: payload.recent_orders ?? [],
|
||||||
recent_disputes: payload.recent_disputes ?? [],
|
recent_disputes: payload.recent_disputes ?? [],
|
||||||
|
|||||||
@@ -335,6 +335,21 @@
|
|||||||
gap: 12px;
|
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 {
|
.listing-stat-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -422,6 +437,18 @@
|
|||||||
background: linear-gradient(180deg, #60a5fa, #3b82f6);
|
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 {
|
.mini-bar-col em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ import {
|
|||||||
Document,
|
Document,
|
||||||
Grid,
|
Grid,
|
||||||
Loading,
|
Loading,
|
||||||
|
Medal,
|
||||||
Operation,
|
Operation,
|
||||||
|
Present,
|
||||||
Refresh,
|
Refresh,
|
||||||
ScaleToOriginal,
|
ScaleToOriginal,
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
Shop,
|
Shop,
|
||||||
Tickets,
|
Tickets,
|
||||||
Top,
|
Top,
|
||||||
|
Trophy,
|
||||||
|
Umbrella,
|
||||||
User,
|
User,
|
||||||
Van,
|
Van,
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -27,6 +31,7 @@ import {
|
|||||||
type ListingDayStats,
|
type ListingDayStats,
|
||||||
type ListingUploaderDayStats,
|
type ListingUploaderDayStats,
|
||||||
type ManualEntryTodayStats,
|
type ManualEntryTodayStats,
|
||||||
|
type TradingDayStats,
|
||||||
} from '@/features/admin/api/adminDashboard'
|
} from '@/features/admin/api/adminDashboard'
|
||||||
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
|
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
|
||||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
@@ -43,6 +48,18 @@ const {
|
|||||||
fetchFn: fetchAdminDashboard,
|
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 listingTrend = computed(() => dashboard.value?.listing_daily?.trend ?? [])
|
||||||
const listingChannelTrend = computed(() => dashboard.value?.listing_daily?.channel_trend ?? [])
|
const listingChannelTrend = computed(() => dashboard.value?.listing_daily?.channel_trend ?? [])
|
||||||
const listingUploaderTrend = computed(() => dashboard.value?.listing_daily?.uploader_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)
|
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) {
|
function barHeight(value: number, max: number) {
|
||||||
if (max <= 0) return '8%'
|
if (max <= 0) return '8%'
|
||||||
return `${Math.max(8, Math.round((value / max) * 100))}%`
|
return `${Math.max(8, Math.round((value / max) * 100))}%`
|
||||||
@@ -194,6 +216,162 @@ function shortDate(date: string) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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 日趋势 -->
|
<!-- 账号上下架:今日三组 + 近 7 日趋势 -->
|
||||||
<section v-if="dashboard" class="section-card">
|
<section v-if="dashboard" class="section-card">
|
||||||
<div class="section-header-row">
|
<div class="section-header-row">
|
||||||
|
|||||||
Reference in New Issue
Block a user