front/src/api/monitor.ts
zzh 16ae7d2192 feat: 工作台与实时监控对接真实订单统计数据
- 工作台 KPI 与订单趋势接入 workplace-stats/trend API

- 实时监控页接入 monitor/overview,含订单/客户/轨迹统计与图表

- 新增 monitor/workplace API 封装及 nginx/vite 代理配置

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 18:24:47 +08:00

207 lines
7.3 KiB
TypeScript

import { axiosInstance } from '@/api/rbac'
import { resolveAdminApiPath } from '@/utils/adminApi'
function monitorUrl(path: string): string {
return resolveAdminApiPath(path)
}
function assertBusinessOk(body: unknown) {
if (body == null) throw new Error('空响应')
const payload = body as Record<string, unknown>
const code = payload.code ?? payload.Code
const ok =
code === undefined ||
code === null ||
code === 0 ||
code === 200 ||
code === '0' ||
code === '200' ||
payload.success === true ||
payload.Success === true
if (!ok) {
throw new Error(
String(payload.msg ?? payload.Msg ?? payload.message ?? payload.Message ?? '请求失败'),
)
}
}
function num(raw: unknown): number {
const value = Number(raw ?? 0)
return Number.isFinite(value) ? value : 0
}
function growth(raw: unknown): number | null {
if (raw === null || raw === undefined || raw === '') return null
const value = Number(raw)
return Number.isFinite(value) ? value : null
}
function normalizeRegionTrend(raw: unknown) {
const bundle = (raw ?? {}) as Record<string, unknown>
const normalizePeriod = (periodRaw: unknown) => {
const period = (periodRaw ?? {}) as Record<string, unknown>
const labels = period.labels ?? period.Labels
const seriesRaw = period.series ?? period.Series
return {
labels: Array.isArray(labels) ? labels.map(String) : [],
series: Array.isArray(seriesRaw)
? seriesRaw.map((item) => {
const row = item as Record<string, unknown>
const values = row.values ?? row.Values
return {
region: String(row.region ?? row.Region ?? ''),
values: Array.isArray(values) ? values.map((v) => num(v)) : [],
}
})
: [],
}
}
return {
week: normalizePeriod(bundle.week ?? bundle.Week),
month: normalizePeriod(bundle.month ?? bundle.Month),
}
}
export interface MonitorOverview {
order: {
todayCreated: number
todayCreatedGrowthPercent: number | null
total: number
totalMonthGrowthPercent: number | null
pendingPayment: number
pendingOnline: number
pendingSign: number
problematic: number
todayRevenue: number
todayRevenueGrowthPercent: number | null
}
customer: {
totalCustomers: number
totalCustomersMonthGrowthPercent: number | null
newCustomersToday: number
newCustomersGrowthPercent: number | null
onlineCustomers: number
inactiveLogin7d: number
inactiveLogin7dDelta: number
}
orderTrend7d: Array<{ label: string; count: number }>
revenueTrend7d: Array<{ label: string; revenue: number; grossProfit: number }>
regionOrderTrend: ReturnType<typeof normalizeRegionTrend>
regionReceivableTrend: ReturnType<typeof normalizeRegionTrend>
regionSignRates: Array<{ region: string; rate: number }>
trackRates: {
trackCompleteRate: number
trackTimelyRate: number
otdRate: number
}
}
function normalizeOverview(raw: Record<string, unknown>): MonitorOverview {
const order = (raw.order ?? raw.Order ?? {}) as Record<string, unknown>
const customer = (raw.customer ?? raw.Customer ?? {}) as Record<string, unknown>
const track = (raw.trackRates ?? raw.TrackRates ?? {}) as Record<string, unknown>
const orderTrendRaw = raw.orderTrend7d ?? raw.OrderTrend7d
const revenueTrendRaw = raw.revenueTrend7d ?? raw.RevenueTrend7d
const signRatesRaw = raw.regionSignRates ?? raw.RegionSignRates
return {
order: {
todayCreated: num(order.todayCreated ?? order.TodayCreated),
todayCreatedGrowthPercent: growth(order.todayCreatedGrowthPercent ?? order.TodayCreatedGrowthPercent),
total: num(order.total ?? order.Total),
totalMonthGrowthPercent: growth(order.totalMonthGrowthPercent ?? order.TotalMonthGrowthPercent),
pendingPayment: num(order.pendingPayment ?? order.PendingPayment),
pendingOnline: num(order.pendingOnline ?? order.PendingOnline),
pendingSign: num(order.pendingSign ?? order.PendingSign),
problematic: num(order.problematic ?? order.Problematic),
todayRevenue: num(order.todayRevenue ?? order.TodayRevenue),
todayRevenueGrowthPercent: growth(order.todayRevenueGrowthPercent ?? order.TodayRevenueGrowthPercent),
},
customer: {
totalCustomers: num(customer.totalCustomers ?? customer.TotalCustomers),
totalCustomersMonthGrowthPercent: growth(
customer.totalCustomersMonthGrowthPercent ?? customer.TotalCustomersMonthGrowthPercent,
),
newCustomersToday: num(customer.newCustomersToday ?? customer.NewCustomersToday),
newCustomersGrowthPercent: growth(
customer.newCustomersGrowthPercent ?? customer.NewCustomersGrowthPercent,
),
onlineCustomers: num(customer.onlineCustomers ?? customer.OnlineCustomers),
inactiveLogin7d: num(customer.inactiveLogin7d ?? customer.InactiveLogin7d),
inactiveLogin7dDelta: num(customer.inactiveLogin7dDelta ?? customer.InactiveLogin7dDelta),
},
orderTrend7d: Array.isArray(orderTrendRaw)
? orderTrendRaw.map((item) => {
const row = item as Record<string, unknown>
return {
label: String(row.label ?? row.Label ?? ''),
count: num(row.count ?? row.Count),
}
})
: [],
revenueTrend7d: Array.isArray(revenueTrendRaw)
? revenueTrendRaw.map((item) => {
const row = item as Record<string, unknown>
return {
label: String(row.label ?? row.Label ?? ''),
revenue: num(row.revenue ?? row.Revenue),
grossProfit: num(row.grossProfit ?? row.GrossProfit),
}
})
: [],
regionOrderTrend: normalizeRegionTrend(raw.regionOrderTrend ?? raw.RegionOrderTrend),
regionReceivableTrend: normalizeRegionTrend(raw.regionReceivableTrend ?? raw.RegionReceivableTrend),
regionSignRates: Array.isArray(signRatesRaw)
? signRatesRaw.map((item) => {
const row = item as Record<string, unknown>
return {
region: String(row.region ?? row.Region ?? ''),
rate: num(row.rate ?? row.Rate),
}
})
: [],
trackRates: {
trackCompleteRate: num(track.trackCompleteRate ?? track.TrackCompleteRate),
trackTimelyRate: num(track.trackTimelyRate ?? track.TrackTimelyRate),
otdRate: num(track.otdRate ?? track.OtdRate),
},
}
}
/** 实时监控概览 GET /monitor/overview */
export async function getMonitorOverview(): Promise<MonitorOverview> {
const res = await axiosInstance.get(monitorUrl('/monitor/overview'))
assertBusinessOk(res.data)
const payload = res.data as Record<string, unknown>
const data = (payload.data ?? payload.Data ?? payload) as Record<string, unknown>
return normalizeOverview(data)
}
export function formatMonitorNumber(value: number): string {
return Number(value || 0).toLocaleString()
}
export function formatMonitorCurrency(value: number): string {
return `¥ ${Number(value || 0).toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})}`
}
export function formatMonitorGrowth(value: number | null | undefined): {
show: boolean
up: boolean
text: string
} {
if (value == null || Number.isNaN(Number(value))) {
return { show: false, up: true, text: '' }
}
const numValue = Number(value)
return {
show: true,
up: numValue >= 0,
text: `${Math.abs(numValue).toFixed(1)}%`,
}
}