diff --git a/nginx-default.conf b/nginx-default.conf index 957d4c9..55bb063 100644 --- a/nginx-default.conf +++ b/nginx-default.conf @@ -181,7 +181,7 @@ server { } # ============ Admin API 同源反代(/rbac /order /xhs … → api.nanafa.com/admin)============ - location ~ ^/(rbac|order|customer|recharge|channel|system)/ { + location ~ ^/(rbac|order|monitor|customer|recharge|channel|system)/ { rewrite ^/(.*)$ /admin/$1 break; proxy_pass http://api.nanafa.com; proxy_http_version 1.1; diff --git a/nginx-docker.conf b/nginx-docker.conf index 0c2eb49..72a85ee 100644 --- a/nginx-docker.conf +++ b/nginx-docker.conf @@ -83,7 +83,7 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } - location ~ ^/(rbac|order|customer|recharge|channel|system)/ { + location ~ ^/(rbac|order|monitor|customer|recharge|channel|system)/ { rewrite ^/(.*)$ /admin/$1 break; proxy_pass http://api.nanafa.com; proxy_http_version 1.1; diff --git a/src/api/monitor.ts b/src/api/monitor.ts new file mode 100644 index 0000000..4afe118 --- /dev/null +++ b/src/api/monitor.ts @@ -0,0 +1,206 @@ +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 + 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 + const normalizePeriod = (periodRaw: unknown) => { + const period = (periodRaw ?? {}) as Record + 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 + 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 + regionReceivableTrend: ReturnType + regionSignRates: Array<{ region: string; rate: number }> + trackRates: { + trackCompleteRate: number + trackTimelyRate: number + otdRate: number + } +} + +function normalizeOverview(raw: Record): MonitorOverview { + const order = (raw.order ?? raw.Order ?? {}) as Record + const customer = (raw.customer ?? raw.Customer ?? {}) as Record + const track = (raw.trackRates ?? raw.TrackRates ?? {}) as Record + 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 + 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 + 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 + 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 { + const res = await axiosInstance.get(monitorUrl('/monitor/overview')) + assertBusinessOk(res.data) + const payload = res.data as Record + const data = (payload.data ?? payload.Data ?? payload) as Record + 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)}%`, + } +} diff --git a/src/api/workplace.ts b/src/api/workplace.ts new file mode 100644 index 0000000..1f76a18 --- /dev/null +++ b/src/api/workplace.ts @@ -0,0 +1,115 @@ +import { axiosInstance } from '@/api/rbac' +import { resolveAdminApiPath } from '@/utils/adminApi' + +function orderUrl(path: string): string { + return resolveAdminApiPath(path) +} + +function assertBusinessOk(body: unknown) { + if (body == null) throw new Error('空响应') + const payload = body as Record + 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 unwrapData(body: unknown): T { + assertBusinessOk(body) + const payload = body as Record + return (payload.data ?? payload.Data ?? body) as T +} + +export interface OrderWorkplaceStats { + total: number + counts: Record + todayCreated: number + yesterdayCreated: number + dayOverDayGrowthPercent: number | null + pendingShip: number + shipped: number + pendingOnline: number + problematic: number +} + +export interface OrderWorkplaceTrendPoint { + label: string + forecastCount: number + shippedCount: number +} + +export interface OrderWorkplaceTrend { + dimension: string + points: OrderWorkplaceTrendPoint[] +} + +function normalizeStats(raw: Record): OrderWorkplaceStats { + const countsRaw = raw.counts ?? raw.Counts + return { + total: Number(raw.total ?? raw.Total ?? 0) || 0, + counts: + typeof countsRaw === 'object' && countsRaw !== null + ? (countsRaw as Record) + : {}, + todayCreated: Number(raw.todayCreated ?? raw.TodayCreated ?? 0) || 0, + yesterdayCreated: Number(raw.yesterdayCreated ?? raw.YesterdayCreated ?? 0) || 0, + dayOverDayGrowthPercent: + raw.dayOverDayGrowthPercent ?? raw.DayOverDayGrowthPercent ?? null, + pendingShip: Number(raw.pendingShip ?? raw.PendingShip ?? 0) || 0, + shipped: Number(raw.shipped ?? raw.Shipped ?? 0) || 0, + pendingOnline: Number(raw.pendingOnline ?? raw.PendingOnline ?? 0) || 0, + problematic: Number(raw.problematic ?? raw.Problematic ?? 0) || 0, + } +} + +function normalizeTrend(raw: Record): OrderWorkplaceTrend { + const pointsRaw = raw.points ?? raw.Points + const points = Array.isArray(pointsRaw) + ? pointsRaw.map((item) => { + const row = item as Record + return { + label: String(row.label ?? row.Label ?? ''), + forecastCount: Number(row.forecastCount ?? row.ForecastCount ?? 0) || 0, + shippedCount: Number(row.shippedCount ?? row.ShippedCount ?? 0) || 0, + } + }) + : [] + + return { + dimension: String(raw.dimension ?? raw.Dimension ?? 'day'), + points, + } +} + +/** 工作台订单统计 GET /order/workplace-stats */ +export async function getOrderWorkplaceStats(): Promise { + const res = await axiosInstance.get(orderUrl('/order/workplace-stats')) + const data = unwrapData>(res.data) + return normalizeStats(data) +} + +/** 工作台订单趋势 GET /order/workplace-trend */ +export async function getOrderWorkplaceTrend(params: { + dimension: 'day' | 'week' | 'month' + count?: number +}): Promise { + const res = await axiosInstance.get(orderUrl('/order/workplace-trend'), { + params: { + dimension: params.dimension, + count: params.count, + }, + }) + const data = unwrapData>(res.data) + return normalizeTrend(data) +} diff --git a/src/views/home/monitor/index.vue b/src/views/home/monitor/index.vue index dd74f76..bf96343 100644 --- a/src/views/home/monitor/index.vue +++ b/src/views/home/monitor/index.vue @@ -6,6 +6,7 @@
+ - +
-
{{ $t('monitor.logistics.stat.todayOrders') }}
-
- -
-
-
1,896
- -
-
-
- - - -
-
-
{{ $t('monitor.logistics.stat.historicalOrders') }}
-
- -
-
-
128,460
- -
-
-
- - - -
-
-
{{ $t('monitor.logistics.stat.pendingPayment') }}
-
- -
-
-
56
- -
-
-
- - - -
-
-
{{ $t('monitor.logistics.stat.pendingOnline') }}
-
- -
-
-
312
- -
-
-
- - - -
-
-
{{ $t('monitor.logistics.stat.pendingSign') }}
-
- -
-
-
892
- -
-
-
- - - -
-
-
{{ $t('monitor.logistics.stat.orderExceptions') }}
-
- -
-
-
23
- -
-
-
- - - -
-
-
{{ $t('monitor.logistics.stat.todayRevenue') }}
+
{{ $t(card.labelKey) }}
-
¥ 126,390
-