feat: 工作台与实时监控对接真实订单统计数据

- 工作台 KPI 与订单趋势接入 workplace-stats/trend API

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-04 18:24:47 +08:00
parent 696adf7684
commit 16ae7d2192
10 changed files with 841 additions and 466 deletions

View File

@ -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;

View File

@ -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;

206
src/api/monitor.ts Normal file
View File

@ -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<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)}%`,
}
}

115
src/api/workplace.ts Normal file
View File

@ -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<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 unwrapData<T>(body: unknown): T {
assertBusinessOk(body)
const payload = body as Record<string, unknown>
return (payload.data ?? payload.Data ?? body) as T
}
export interface OrderWorkplaceStats {
total: number
counts: Record<string, number>
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<string, unknown>): 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<string, number>)
: {},
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<string, unknown>): OrderWorkplaceTrend {
const pointsRaw = raw.points ?? raw.Points
const points = Array.isArray(pointsRaw)
? pointsRaw.map((item) => {
const row = item as Record<string, unknown>
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<OrderWorkplaceStats> {
const res = await axiosInstance.get(orderUrl('/order/workplace-stats'))
const data = unwrapData<Record<string, unknown>>(res.data)
return normalizeStats(data)
}
/** 工作台订单趋势 GET /order/workplace-trend */
export async function getOrderWorkplaceTrend(params: {
dimension: 'day' | 'week' | 'month'
count?: number
}): Promise<OrderWorkplaceTrend> {
const res = await axiosInstance.get(orderUrl('/order/workplace-trend'), {
params: {
dimension: params.dimension,
count: params.count,
},
})
const data = unwrapData<Record<string, unknown>>(res.data)
return normalizeTrend(data)
}

View File

@ -6,6 +6,7 @@
</div>
<div class="logistics-monitor-body">
<a-spin :loading="overviewLoading" style="width: 100%">
<a-tabs
v-model:active-key="monitorTab"
type="rounded"
@ -15,140 +16,29 @@
>
<a-tab-pane key="order" :title="$t('monitor.logistics.tab.order')">
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-col
v-for="card in orderStatCards"
:key="card.key"
:xs="24"
:sm="12"
:md="12"
:lg="12"
:xl="6"
>
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.todayOrders') }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<icon-calendar-clock />
</div>
</div>
<div class="stat-value">1,896</div>
<div class="stat-footer">
<div class="stat-trend up">
<icon-arrow-up />
12.5% {{ $t('monitor.logistics.vsYesterday') }}
</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.historicalOrders') }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<icon-history />
</div>
</div>
<div class="stat-value">128,460</div>
<div class="stat-footer">
<div class="stat-trend up">
<icon-arrow-up />
2.1% {{ $t('monitor.logistics.vsLastMonth') }}
</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.pendingPayment') }}</div>
<div class="stat-item__icon stat-item__icon--warning">
<icon-stamp />
</div>
</div>
<div class="stat-value stat-value--warning">56</div>
<div class="stat-footer">
<div class="stat-trend down">
<icon-arrow-down />
4.0% {{ $t('monitor.logistics.vsYesterday') }}
</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.pendingOnline') }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<icon-send />
</div>
</div>
<div class="stat-value">312</div>
<div class="stat-footer">
<div class="stat-trend up">
<icon-arrow-up />
3.6% {{ $t('monitor.logistics.vsYesterday') }}
</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.pendingSign') }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<icon-check-circle />
</div>
</div>
<div class="stat-value">892</div>
<div class="stat-footer">
<div class="stat-trend down">
<icon-arrow-down />
1.2% {{ $t('monitor.logistics.vsYesterday') }}
</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.orderExceptions') }}</div>
<div class="stat-item__icon stat-item__icon--warning">
<icon-exclamation-circle />
</div>
</div>
<div class="stat-value stat-value--warning">23</div>
<div class="stat-footer">
<div class="stat-trend down">
<icon-arrow-down />
8.2% {{ $t('monitor.logistics.vsYesterday') }}
</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.todayRevenue') }}</div>
<div class="stat-label">{{ $t(card.labelKey) }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<icon-bar-chart />
</div>
</div>
<div class="stat-value">¥ 126,390</div>
<div class="stat-footer">
<div class="stat-trend up">
<icon-arrow-up />
18.7% {{ $t('monitor.logistics.vsYesterday') }}
<div class="stat-value" :class="{ 'stat-value--warning': card.warning }">{{ card.value }}</div>
<div v-if="card.growth.show" class="stat-footer">
<div class="stat-trend" :class="card.growth.up ? 'up' : 'down'">
<icon-arrow-up v-if="card.growth.up" />
<icon-arrow-down v-else />
{{ card.growth.text }} {{ $t(card.compareKey) }}
</div>
</div>
</div>
@ -173,77 +63,41 @@
<a-tab-pane key="customer" :title="$t('monitor.logistics.tab.customer')">
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-col
v-for="card in customerStatCards"
:key="card.key"
:xs="24"
:sm="12"
:md="12"
:lg="12"
:xl="6"
>
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.totalCustomers') }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<div class="stat-label">{{ $t(card.labelKey) }}</div>
<div
class="stat-item__icon"
:class="card.warning ? 'stat-item__icon--warning' : card.icon === 'online' ? 'stat-item__icon--online' : 'stat-item__icon--primary'"
>
<icon-user-group />
</div>
</div>
<div class="stat-value">12,480</div>
<div class="stat-value" :class="{ 'stat-value--warning': card.warning }">{{ card.value }}</div>
<div class="stat-footer">
<div class="stat-trend up">
<icon-arrow-up />
1.2% {{ $t('monitor.logistics.vsLastMonth') }}
<div v-if="card.growth.show" class="stat-trend" :class="card.growth.up ? 'up' : 'down'">
<icon-arrow-up v-if="card.growth.up" />
<icon-arrow-down v-else />
{{ card.growth.text }} {{ $t(card.compareKey) }}
</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.newCustomers') }}</div>
<div class="stat-item__icon stat-item__icon--primary">
<icon-user-add />
<div
v-if="card.hintKey"
class="stat-hint"
:class="{ 'stat-hint--accent': card.hintAccent }"
>
{{ $t(card.hintKey) }}
</div>
</div>
<div class="stat-value">86</div>
<div class="stat-footer">
<div class="stat-trend up">
<icon-arrow-up />
6.2% {{ $t('monitor.logistics.vsYesterday') }}
</div>
<div class="stat-hint">{{ $t('monitor.logistics.hint.newCustomersToday') }}</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.onlineCustomers') }}</div>
<div class="stat-item__icon stat-item__icon--online">
<icon-thunderbolt />
</div>
</div>
<div class="stat-value">342</div>
<div class="stat-footer">
<div class="stat-hint stat-hint--accent">{{ $t('monitor.logistics.hint.onlineRealtime') }}</div>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6">
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
<div class="stat-item">
<div class="stat-item__head">
<div class="stat-label">{{ $t('monitor.logistics.stat.inactiveLogin7d') }}</div>
<div class="stat-item__icon stat-item__icon--warning">
<icon-clock-circle />
</div>
</div>
<div class="stat-value stat-value--warning">156</div>
<div class="stat-footer">
<div class="stat-hint">{{ $t('monitor.logistics.hint.inactiveLogin7d') }}</div>
<div class="stat-hint">{{ $t('monitor.logistics.hint.inactiveLogin7dCompare') }}</div>
<div v-if="card.extraHintKey" class="stat-hint">{{ $t(card.extraHintKey) }}</div>
</div>
</div>
</a-card>
@ -365,6 +219,7 @@
</a-row>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
</div>
</div>
@ -375,11 +230,155 @@
import * as echarts from 'echarts';
import { modifyAlpha } from 'zrender/lib/tool/color';
import { useI18n } from 'vue-i18n';
import {
getMonitorOverview,
formatMonitorNumber,
formatMonitorCurrency,
formatMonitorGrowth,
type MonitorOverview,
} from '@/api/monitor';
defineOptions({ name: 'Monitor' });
const { t } = useI18n();
const overview = ref<MonitorOverview | null>(null);
const overviewLoading = ref(true);
async function loadOverview() {
overviewLoading.value = true;
try {
overview.value = await getMonitorOverview();
} catch {
overview.value = null;
} finally {
overviewLoading.value = false;
await nextTick();
refreshChartsForTab(monitorTab.value);
}
}
function refreshChartsForTab(tab: 'customer' | 'order' | 'track') {
if (tab === 'order') ensureOrderLineCharts();
if (tab === 'track') ensureTrackCharts();
if (tab === 'customer') ensureCustomerCharts();
nextTick(() => onResize());
}
const orderStatCards = computed(() => {
const order = overview.value?.order;
return [
{
key: 'todayOrders',
labelKey: 'monitor.logistics.stat.todayOrders',
value: formatMonitorNumber(order?.todayCreated ?? 0),
growth: formatMonitorGrowth(order?.todayCreatedGrowthPercent),
compareKey: 'monitor.logistics.vsYesterday',
warning: false,
icon: 'calendar',
},
{
key: 'historicalOrders',
labelKey: 'monitor.logistics.stat.historicalOrders',
value: formatMonitorNumber(order?.total ?? 0),
growth: formatMonitorGrowth(order?.totalMonthGrowthPercent),
compareKey: 'monitor.logistics.vsLastMonth',
warning: false,
icon: 'history',
},
{
key: 'pendingPayment',
labelKey: 'monitor.logistics.stat.pendingPayment',
value: formatMonitorNumber(order?.pendingPayment ?? 0),
growth: { show: false, up: true, text: '' },
compareKey: 'monitor.logistics.vsYesterday',
warning: true,
icon: 'stamp',
},
{
key: 'pendingOnline',
labelKey: 'monitor.logistics.stat.pendingOnline',
value: formatMonitorNumber(order?.pendingOnline ?? 0),
growth: { show: false, up: true, text: '' },
compareKey: 'monitor.logistics.vsYesterday',
warning: false,
icon: 'send',
},
{
key: 'pendingSign',
labelKey: 'monitor.logistics.stat.pendingSign',
value: formatMonitorNumber(order?.pendingSign ?? 0),
growth: { show: false, up: true, text: '' },
compareKey: 'monitor.logistics.vsYesterday',
warning: false,
icon: 'check',
},
{
key: 'orderExceptions',
labelKey: 'monitor.logistics.stat.orderExceptions',
value: formatMonitorNumber(order?.problematic ?? 0),
growth: { show: false, up: false, text: '' },
compareKey: 'monitor.logistics.vsYesterday',
warning: true,
icon: 'exception',
},
{
key: 'todayRevenue',
labelKey: 'monitor.logistics.stat.todayRevenue',
value: formatMonitorCurrency(order?.todayRevenue ?? 0),
growth: formatMonitorGrowth(order?.todayRevenueGrowthPercent),
compareKey: 'monitor.logistics.vsYesterday',
warning: false,
icon: 'chart',
},
];
});
const customerStatCards = computed(() => {
const customer = overview.value?.customer;
return [
{
key: 'totalCustomers',
labelKey: 'monitor.logistics.stat.totalCustomers',
value: formatMonitorNumber(customer?.totalCustomers ?? 0),
growth: formatMonitorGrowth(customer?.totalCustomersMonthGrowthPercent),
compareKey: 'monitor.logistics.vsLastMonth',
warning: false,
icon: 'group',
},
{
key: 'newCustomers',
labelKey: 'monitor.logistics.stat.newCustomers',
value: formatMonitorNumber(customer?.newCustomersToday ?? 0),
growth: formatMonitorGrowth(customer?.newCustomersGrowthPercent),
compareKey: 'monitor.logistics.vsYesterday',
warning: false,
icon: 'add',
hintKey: 'monitor.logistics.hint.newCustomersToday',
},
{
key: 'onlineCustomers',
labelKey: 'monitor.logistics.stat.onlineCustomers',
value: formatMonitorNumber(customer?.onlineCustomers ?? 0),
growth: { show: false, up: true, text: '' },
warning: false,
icon: 'online',
hintKey: 'monitor.logistics.hint.onlineRealtime',
hintAccent: true,
},
{
key: 'inactiveLogin7d',
labelKey: 'monitor.logistics.stat.inactiveLogin7d',
value: formatMonitorNumber(customer?.inactiveLogin7d ?? 0),
growth: { show: false, up: true, text: '' },
warning: true,
icon: 'clock',
hintKey: 'monitor.logistics.hint.inactiveLogin7d',
extraHintKey: 'monitor.logistics.hint.inactiveLogin7dCompare',
},
];
});
const LINE_KEYS = ['us', 'eu', 'sea', 'au', 'jp', 'uk'] as const;
type LineKey = (typeof LINE_KEYS)[number];
@ -449,7 +448,6 @@
);
function buildSignRateSeries(): { categories: string[]; values: number[] } {
const days = filterSignDays.value;
const lineKey = filterLine.value;
const keys: LineKey[] =
!lineKey || lineKey === 'all'
@ -458,16 +456,16 @@
? [lineKey as LineKey]
: [...LINE_KEYS];
const dayFactor = 1 + (days - 7) * 0.0045;
const rateMap = new Map(
(overview.value?.regionSignRates ?? []).map((item) => [item.region, item.rate]),
);
const categories: string[] = [];
const values: number[] = [];
keys.forEach((key) => {
const raw = baseSignRates[key] * dayFactor + (keys.length === 1 ? 0.3 : 0);
const v = Math.min(100, Math.max(0, Number(raw.toFixed(1))));
categories.push(t(`monitor.logistics.line.${key}`));
values.push(v);
values.push(Number(rateMap.get(key) ?? 0));
});
return { categories, values };
@ -538,15 +536,17 @@
return LINE_KEYS.includes(v as LineKey) ? [v as LineKey] : [...LINE_KEYS];
}
function getPiePassRate(metric: PieMetricKey): number {
const rates = overview.value?.trackRates;
if (!rates) return 0;
if (metric === 'complete') return rates.trackCompleteRate;
if (metric === 'timely') return rates.trackTimelyRate;
return rates.otdRate;
}
function averagePieMetric(metric: PieMetricKey): number {
const keys = getActiveLineKeysForPie();
const periodMul = piePeriod.value === 'month' ? 1.012 : 0.992;
let sum = 0;
keys.forEach((k) => {
sum += pieMetricBase[k][metric];
});
const raw = (sum / keys.length) * periodMul;
return Math.min(99.5, Math.max(70, Number(raw.toFixed(1))));
const pass = getPiePassRate(metric);
return Math.min(99.5, Math.max(0, Number(pass.toFixed(1))));
}
function buildPieChartOption(passColor: string, metric: PieMetricKey) {
@ -670,35 +670,52 @@
return orders.map((o) => Math.round(o * unitPrice));
}
function getRegionTrendBundle(kind: 'order' | 'receivable', period: 'week' | 'month') {
const bundle =
kind === 'order' ? overview.value?.regionOrderTrend : overview.value?.regionReceivableTrend;
return period === 'month' ? bundle?.month : bundle?.week;
}
function renderCustomerTrendCharts() {
const period = customerTrendPeriod.value;
const categories = buildCustomerPeriodLabels(period);
const orderTrend = getRegionTrendBundle('order', period);
const receivableTrend = getRegionTrendBundle('receivable', period);
const categories =
orderTrend?.labels?.length
? orderTrend.labels
: buildCustomerPeriodLabels(period);
const keys = getActiveCustomerLineKeys();
const colors = getLineTrendColors();
const axisLine = readCssVar('--color-border-2', '#e5e6eb');
const text2 = readCssVar('--color-text-2', '#86909c');
const orderSeries = keys.map((key) => ({
name: t(`monitor.logistics.line.${key}`),
type: 'line' as const,
smooth: true,
symbol: 'circle',
symbolSize: keys.length > 2 ? 4 : 6,
data: buildMockOrderSeries(key, period, categories.length),
itemStyle: { color: colors[LINE_KEYS.indexOf(key) % colors.length] },
lineStyle: { width: keys.length > 2 ? 2 : 2.5 },
}));
const orderSeries = keys.map((key) => {
const matched = orderTrend?.series?.find((item) => item.region === key);
return {
name: t(`monitor.logistics.line.${key}`),
type: 'line' as const,
smooth: true,
symbol: 'circle',
symbolSize: keys.length > 2 ? 4 : 6,
data: matched?.values?.length ? matched.values : Array(categories.length).fill(0),
itemStyle: { color: colors[LINE_KEYS.indexOf(key) % colors.length] },
lineStyle: { width: keys.length > 2 ? 2 : 2.5 },
};
});
const receivableSeries = keys.map((key) => ({
name: t(`monitor.logistics.line.${key}`),
type: 'line' as const,
smooth: true,
symbol: 'circle',
symbolSize: keys.length > 2 ? 4 : 6,
data: buildMockReceivableSeries(key, period, categories.length),
itemStyle: { color: colors[LINE_KEYS.indexOf(key) % colors.length] },
lineStyle: { width: keys.length > 2 ? 2 : 2.5 },
}));
const receivableSeries = keys.map((key) => {
const matched = receivableTrend?.series?.find((item) => item.region === key);
return {
name: t(`monitor.logistics.line.${key}`),
type: 'line' as const,
smooth: true,
symbol: 'circle',
symbolSize: keys.length > 2 ? 4 : 6,
data: matched?.values?.length ? matched.values : Array(categories.length).fill(0),
itemStyle: { color: colors[LINE_KEYS.indexOf(key) % colors.length] },
lineStyle: { width: keys.length > 2 ? 2 : 2.5 },
};
});
const grid = { left: '3%', right: '3%', bottom: keys.length > 1 ? '14%' : '3%', top: '10%', containLabel: true };
@ -774,34 +791,37 @@
function ensureOrderLineCharts() {
const primary = readCssVar('--app-primary-color', '#2D8C8C');
const axisLine = readCssVar('--color-border-2', '#e5e6eb');
const dayLabels = [
t('monitor.logistics.axis.d1'),
t('monitor.logistics.axis.d2'),
t('monitor.logistics.axis.d3'),
t('monitor.logistics.axis.d4'),
t('monitor.logistics.axis.d5'),
t('monitor.logistics.axis.d6'),
t('monitor.logistics.axis.today'),
];
const orderTrend = overview.value?.orderTrend7d ?? [];
const revenueTrend = overview.value?.revenueTrend7d ?? [];
const dayLabels = orderTrend.length
? orderTrend.map((item) => item.label)
: revenueTrend.map((item) => item.label);
const orderValues = orderTrend.length
? orderTrend.map((item) => item.count)
: [];
const revenueValues = revenueTrend.map((item) => item.revenue);
const grossValues = revenueTrend.map((item) => item.grossProfit);
if (orderChartRef.value) {
if (!orderChartInstance) {
orderChartInstance = echarts.init(orderChartRef.value);
orderChartInstance.setOption({
}
orderChartInstance.setOption(
{
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: dayLabels,
data: dayLabels.length ? dayLabels : [t('workplace.record.nodata')],
axisLine: { lineStyle: { color: axisLine } },
},
yAxis: { type: 'value', axisLine: { lineStyle: { color: axisLine } } },
yAxis: { type: 'value', minInterval: 1, axisLine: { lineStyle: { color: axisLine } } },
series: [
{
name: t('monitor.logistics.series.orders'),
type: 'line',
smooth: true,
data: [1230, 1350, 1420, 1580, 1660, 1790, 1896],
data: orderValues,
itemStyle: { color: primary },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@ -811,16 +831,19 @@
},
},
],
});
}
},
{ notMerge: true },
);
}
if (profitChartRef.value) {
if (!profitChartInstance) {
profitChartInstance = echarts.init(profitChartRef.value);
const revenueColor = '#00B42A';
const grossColor = '#FF7D00';
profitChartInstance.setOption({
}
const revenueColor = '#00B42A';
const grossColor = '#FF7D00';
profitChartInstance.setOption(
{
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
tooltip: { trigger: 'axis' },
legend: {
@ -829,7 +852,7 @@
},
xAxis: {
type: 'category',
data: dayLabels,
data: dayLabels.length ? dayLabels : [t('workplace.record.nodata')],
axisLine: { lineStyle: { color: axisLine } },
},
yAxis: { type: 'value', axisLine: { lineStyle: { color: axisLine } } },
@ -838,19 +861,20 @@
name: t('monitor.logistics.series.revenue'),
type: 'line',
smooth: true,
data: [85600, 92300, 98700, 105200, 111500, 118900, 126390],
data: revenueValues,
itemStyle: { color: revenueColor },
},
{
name: t('monitor.logistics.series.grossProfit'),
type: 'line',
smooth: true,
data: [21400, 23000, 24600, 26200, 27800, 29600, 31500],
data: grossValues,
itemStyle: { color: grossColor },
},
],
});
}
},
{ notMerge: true },
);
}
orderChartInstance?.resize();
@ -873,6 +897,7 @@
layoutResizeObserver = new ResizeObserver(() => onResize());
layoutResizeObserver.observe(logisticsMonitorRoot.value);
}
await loadOverview();
});
function disposeOrderCharts() {

View File

@ -1,169 +1,190 @@
<template>
<div class="order-state-panel">
<div class="panel-col">
<a-space>
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/288b89194e657603ff40db39e8072640.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
:title="$t('workplace.onlineContent')"
:value="373.5"
:precision="1"
show-group-separator
>
<template #suffix>
W+ <span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
</div>
<div class="panel-col">
<a-space>
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/fdc66b07224cdf18843c6076c2587eb5.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
:title="$t('workplace.putIn')"
:value="368"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
</div>
<div class="panel-col">
<a-space>
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77d74c9a245adeae1ec7fb5d4539738d.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
:title="$t('workplace.newDay')"
:value="8874"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
</div>
<div class="panel-col">
<a-space>
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/288b89194e657603ff40db39e8072640.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
:title="$t('workplace.pendingOnline')"
:value="156"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
</div>
<div class="panel-col">
<a-space>
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/c8b36e26d2b9bb5dbf9b74dd6d7345af.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
:title="$t('workplace.problematic')"
:value="12"
show-group-separator
>
<template #suffix>
<span class="unit">{{ $t('workplace.pecs') }}</span>
</template>
</a-statistic>
</a-space>
</div>
<div class="panel-col panel-col--last">
<a-space>
<a-avatar :size="54" class="col-avatar">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/c8b36e26d2b9bb5dbf9b74dd6d7345af.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-statistic
:title="$t('workplace.newFromYesterday')"
:value="2.8"
:precision="1"
>
<template #suffix> % <icon-caret-up class="up-icon" /> </template>
</a-statistic>
</a-space>
</div>
<a-spin v-if="loading" class="order-state-panel__loading" />
<template v-else>
<div
v-for="(item, index) in statItems"
:key="item.key"
class="panel-col"
:class="{ 'panel-col--last': index === statItems.length - 1 }"
>
<a-space>
<a-avatar :size="54" class="col-avatar">
<img alt="" :src="item.icon" />
</a-avatar>
<a-statistic
:title="t(item.titleKey)"
:value="item.value"
:precision="item.precision"
show-group-separator
>
<template v-if="item.suffixKey" #suffix>
<span class="unit">{{ t(item.suffixKey) }}</span>
</template>
<template v-else-if="item.showTrend" #suffix>
% <icon-caret-up v-if="item.trendUp" class="up-icon" />
<icon-caret-down v-else class="down-icon" />
</template>
</a-statistic>
</a-space>
</div>
</template>
<a-divider class="panel-border" />
</div>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { IconCaretDown, IconCaretUp } from '@arco-design/web-vue/es/icon';
import { getOrderWorkplaceStats, type OrderWorkplaceStats } from '@/api/workplace';
const STAT_ICONS = [
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/288b89194e657603ff40db39e8072640.svg~tplv-49unhts6dw-image.image',
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/fdc66b07224cdf18843c6076c2587eb5.svg~tplv-49unhts6dw-image.image',
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77d74c9a245adeae1ec7fb5d4539738d.svg~tplv-49unhts6dw-image.image',
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/288b89194e657603ff40db39e8072640.svg~tplv-49unhts6dw-image.image',
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/c8b36e26d2b9bb5dbf9b74dd6d7345af.svg~tplv-49unhts6dw-image.image',
'//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/c8b36e26d2b9bb5dbf9b74dd6d7345af.svg~tplv-49unhts6dw-image.image',
] as const;
const { t } = useI18n();
const loading = ref(true);
const stats = ref<OrderWorkplaceStats | null>(null);
const statItems = computed(() => {
const data = stats.value;
const growth = data?.dayOverDayGrowthPercent;
const growthValue = growth == null ? 0 : Math.abs(Number(growth));
const growthUp = growth == null ? true : Number(growth) >= 0;
return [
{
key: 'todayCreated',
titleKey: 'workplace.onlineContent',
value: data?.todayCreated ?? 0,
precision: 0,
suffixKey: 'workplace.pecs',
icon: STAT_ICONS[0],
},
{
key: 'pendingShip',
titleKey: 'workplace.putIn',
value: data?.pendingShip ?? 0,
precision: 0,
suffixKey: 'workplace.pecs',
icon: STAT_ICONS[1],
},
{
key: 'shipped',
titleKey: 'workplace.newDay',
value: data?.shipped ?? 0,
precision: 0,
suffixKey: 'workplace.pecs',
icon: STAT_ICONS[2],
},
{
key: 'pendingOnline',
titleKey: 'workplace.pendingOnline',
value: data?.pendingOnline ?? 0,
precision: 0,
suffixKey: 'workplace.pecs',
icon: STAT_ICONS[3],
},
{
key: 'problematic',
titleKey: 'workplace.problematic',
value: data?.problematic ?? 0,
precision: 0,
suffixKey: 'workplace.pecs',
icon: STAT_ICONS[4],
},
{
key: 'growth',
titleKey: 'workplace.newFromYesterday',
value: growthValue,
precision: 1,
showTrend: true,
trendUp: growthUp,
icon: STAT_ICONS[5],
},
];
});
async function loadStats() {
loading.value = true;
try {
stats.value = await getOrderWorkplaceStats();
} catch {
stats.value = null;
} finally {
loading.value = false;
}
}
onMounted(() => {
void loadStats();
});
</script>
<style lang="less" scoped>
.order-state-panel {
position: relative;
margin-bottom: 0;
padding: 16px 20px 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px 0;
min-height: 96px;
}
.order-state-panel__loading {
grid-column: 1 / -1;
display: flex;
justify-content: center;
padding: 24px 0;
}
.panel-col {
padding-left: 43px;
border-right: 1px solid rgb(var(--gray-2));
}
.panel-col--last {
border-right: none;
}
.col-avatar {
margin-right: 12px;
background-color: var(--color-fill-2);
}
.up-icon {
color: rgb(var(--red-6));
}
.down-icon {
color: rgb(var(--green-6));
}
.unit {
margin-left: 8px;
color: rgb(var(--gray-8));
font-size: 12px;
}
:deep(.panel-border) {
grid-column: 1 / -1;
margin: 4px 0 0;
}
@media (max-width: 1200px) {
.order-state-panel {
margin-bottom: 0;
padding: 16px 20px 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.panel-col {
padding-left: 43px;
border-right: 1px solid rgb(var(--gray-2));
}
.panel-col--last {
border-right: none;
}
.col-avatar {
margin-right: 12px;
background-color: var(--color-fill-2);
}
.up-icon {
color: rgb(var(--red-6));
}
.unit {
margin-left: 8px;
color: rgb(var(--gray-8));
font-size: 12px;
}
:deep(.panel-border) {
grid-column: 1 / -1;
margin: 4px 0 0;
}
@media (max-width: 1200px) {
.order-state-panel {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.panel-col {
border-right: none;
}
}
}
</style>

View File

@ -7,97 +7,81 @@
}"
>
<template #title>
订单分析
{{ t('workplace.orderAnalysis.title') }}
</template>
<template #extra>
<div class="chart-controls">
<a-radio-group v-model="dimension" type="button" size="small" @change="handleDimensionChange">
<a-radio value="day">日</a-radio>
<a-radio value="week">周</a-radio>
<a-radio value="month">月</a-radio>
<a-radio value="day">{{ t('workplace.orderAnalysis.day') }}</a-radio>
<a-radio value="week">{{ t('workplace.orderAnalysis.week') }}</a-radio>
<a-radio value="month">{{ t('workplace.orderAnalysis.month') }}</a-radio>
</a-radio-group>
</div>
</template>
<Chart class="order-analysis-chart" height="360px" :option="chartOption" />
<a-spin v-if="loading" class="order-analysis-loading" />
<Chart v-else class="order-analysis-chart" height="360px" :option="chartOption" />
</a-card>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import Chart from '@/components/chart/index.vue';
import { useAppStore } from '@/store';
import dayjs from 'dayjs';
import { getOrderWorkplaceTrend } from '@/api/workplace';
const { t } = useI18n();
const dimension = ref<'day' | 'week' | 'month'>('day');
const days = ref(7); // 默认7天
// 生成日期数据
const generateDateLabels = (dim: 'day' | 'week' | 'month', count: number) => {
const labels: string[] = [];
const today = dayjs();
if (dim === 'day') {
for (let i = count - 1; i >= 0; i--) {
labels.push(today.subtract(i, 'day').format('MM-DD'));
}
} else if (dim === 'week') {
for (let i = count - 1; i >= 0; i--) {
const weekStart = today.subtract(i, 'week').startOf('week');
labels.push(`第${weekStart.format('ww')}周`);
}
} else if (dim === 'month') {
for (let i = count - 1; i >= 0; i--) {
labels.push(today.subtract(i, 'month').format('YYYY-MM'));
}
}
return labels;
};
// 生成模拟数据
const generateMockData = (count: number) => {
return Array.from({ length: count }, () => Math.floor(Math.random() * 500) + 100);
};
const xAxisLabels = computed(() => generateDateLabels(dimension.value, days.value));
const pointCount = ref(7);
const loading = ref(true);
const xAxisLabels = ref<string[]>([]);
const forecastData = ref<number[]>([]);
const shippedData = ref<number[]>([]);
const loadData = () => {
const count = days.value;
forecastData.value = generateMockData(count);
shippedData.value = generateMockData(count);
};
async function loadData() {
loading.value = true;
try {
const trend = await getOrderWorkplaceTrend({
dimension: dimension.value,
count: pointCount.value,
});
xAxisLabels.value = trend.points.map((item) => item.label);
forecastData.value = trend.points.map((item) => item.forecastCount);
shippedData.value = trend.points.map((item) => item.shippedCount);
} catch {
xAxisLabels.value = [];
forecastData.value = [];
shippedData.value = [];
} finally {
loading.value = false;
}
}
const handleDimensionChange = () => {
// 根据维度调整天数
if (dimension.value === 'week') {
days.value = 4; // 4周
pointCount.value = 4;
} else if (dimension.value === 'month') {
days.value = 6; // 6个月
pointCount.value = 6;
} else {
days.value = 7; // 7天
pointCount.value = 7;
}
loadData();
void loadData();
};
// 使用 computed 来创建响应式的图表配置
const chartOption = computed(() => {
const appStore = useAppStore();
const isDark = appStore.theme === 'dark';
// 确保数据已准备好
const labels = xAxisLabels.value || [];
const forecast = forecastData.value || [];
const shipped = shippedData.value || [];
// 如果数据为空,返回最小有效配置
if (labels.length === 0 || forecast.length === 0 || shipped.length === 0) {
return {
grid: { left: '3%', right: '4%', top: '20', bottom: '40' },
xAxis: {
type: 'category',
data: labels.length > 0 ? labels : ['暂无数据'],
data: [t('workplace.record.nodata')],
boundaryGap: false,
},
yAxis: {
@ -106,7 +90,7 @@ const chartOption = computed(() => {
series: [],
};
}
return {
grid: {
left: '3%',
@ -146,6 +130,7 @@ const chartOption = computed(() => {
},
yAxis: {
type: 'value',
minInterval: 1,
axisLine: {
show: false,
},
@ -160,7 +145,7 @@ const chartOption = computed(() => {
},
series: [
{
name: '预报订单',
name: t('workplace.orderAnalysis.forecast'),
type: 'line',
smooth: true,
data: forecast,
@ -194,7 +179,7 @@ const chartOption = computed(() => {
},
},
{
name: '发货订单',
name: t('workplace.orderAnalysis.shipped'),
type: 'line',
smooth: true,
data: shipped,
@ -232,7 +217,7 @@ const chartOption = computed(() => {
});
onMounted(() => {
loadData();
void loadData();
});
</script>
@ -243,8 +228,14 @@ onMounted(() => {
gap: 12px;
}
.order-analysis-loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 360px;
}
.order-analysis-chart {
min-height: 360px;
}
</style>

View File

@ -4,12 +4,18 @@ export default {
'workplace.balance': 'Balance (CNY)',
'workplace.order.pending': 'Pending',
'workplace.order.pendingRenewal': 'Renewal Order',
'workplace.onlineContent': 'Online Content',
'workplace.putIn': 'Put In',
'workplace.onlineContent': 'New Orders Today',
'workplace.putIn': 'Pending Shipment',
'workplace.pendingOnline': 'Pending Online',
'workplace.newDay': 'Daily Additional Comments',
'workplace.newDay': 'Shipped',
'workplace.problematic': 'Problematic',
'workplace.newFromYesterday': 'New From Yesterday',
'workplace.newFromYesterday': 'Day-over-Day Growth',
'workplace.orderAnalysis.title': 'Order Analysis',
'workplace.orderAnalysis.day': 'Day',
'workplace.orderAnalysis.week': 'Week',
'workplace.orderAnalysis.month': 'Month',
'workplace.orderAnalysis.forecast': 'New Orders',
'workplace.orderAnalysis.shipped': 'Shipped Orders',
'workplace.minute': 'Min',
'workplace.docs': 'Documents',
'workplace.docs.productOverview': 'Product Overview',
@ -36,7 +42,7 @@ export default {
'workplace.popularContent.image': 'image',
'workplace.popularContent.video': 'video',
'workplace.categoriesPercent': 'Categories Percent',
'workplace.pecs': 'pecs',
'workplace.pecs': 'orders',
'workplace.noticePopup.markRead': 'Mark as read',
'workplace.noticePopup.close': 'Close',
'workplace.noticePopup.readFailed': 'Could not mark as read. Please try again.',

View File

@ -4,12 +4,18 @@ export default {
'workplace.balance': '余额(元)',
'workplace.order.pending': '待支付',
'workplace.order.pendingRenewal': '待续费订单',
'workplace.onlineContent': '今日预报订单',
'workplace.onlineContent': '今日新增订单',
'workplace.putIn': '待发货',
'workplace.pendingOnline': '待上网',
'workplace.newDay': '已发货',
'workplace.problematic': '问题件',
'workplace.newFromYesterday': '较昨日新增',
'workplace.orderAnalysis.title': '订单分析',
'workplace.orderAnalysis.day': '日',
'workplace.orderAnalysis.week': '周',
'workplace.orderAnalysis.month': '月',
'workplace.orderAnalysis.forecast': '新增订单',
'workplace.orderAnalysis.shipped': '发货订单',
'workplace.minute': '分钟',
'workplace.docs': '帮助文档',
'workplace.docs.productOverview': '产品概要',

View File

@ -45,6 +45,11 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
secure: false,
},
'/monitor': {
target: 'http://api.nanafa.com/admin',
changeOrigin: true,
secure: false,
},
'/customer': {
target: 'http://api.nanafa.com/admin',
changeOrigin: true,