All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m41s
对齐登录、工作台、H5、官网与充值页的轻压 inset;监控改胶囊 Tab 减闪;充值页去步骤条并收窄布局。 Co-authored-by: Cursor <cursoragent@cursor.com>
1472 lines
47 KiB
Vue
1472 lines
47 KiB
Vue
<template>
|
||
<div ref="logisticsMonitorRoot" class="logistics-monitor-container">
|
||
<div class="monitor-topbar">
|
||
<h2 class="logistics-monitor-title">{{ $t('monitor.logistics.pageTitle') }}</h2>
|
||
<SlidingTabs
|
||
v-model="monitorTab"
|
||
class="monitor-top-tabs"
|
||
:items="monitorTabItems"
|
||
:aria-label="$t('monitor.logistics.pageTitle')"
|
||
/>
|
||
<div class="monitor-topbar__spacer" />
|
||
<div v-show="monitorTab === 'order'" class="monitor-ops-toolbar">
|
||
<span class="monitor-ops-toolbar__label">{{ $t('monitor.logistics.filter.opsPeriod') }}</span>
|
||
<a-radio-group v-model="opsDimension" type="button" size="small">
|
||
<a-radio value="day">{{ $t('monitor.logistics.filter.periodDay') }}</a-radio>
|
||
<a-radio value="week">{{ $t('monitor.logistics.filter.periodWeek') }}</a-radio>
|
||
<a-radio value="month">{{ $t('monitor.logistics.filter.periodMonth') }}</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="logistics-monitor-body">
|
||
<a-spin :loading="overviewLoading" style="width: 100%; height: 100%">
|
||
<div class="monitor-shell">
|
||
<div class="monitor-tab-panels">
|
||
<div v-show="monitorTab === 'order'" class="monitor-tab-panel">
|
||
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
|
||
<a-col
|
||
v-for="card in opsStatCards"
|
||
:key="card.key"
|
||
:xs="24"
|
||
:sm="12"
|
||
:md="8"
|
||
:lg="8"
|
||
:xl="8"
|
||
>
|
||
<a-card hoverable class="stat-card" :body-style="{ padding: '12px 14px 10px', height: '100%' }">
|
||
<div class="stat-item">
|
||
<div class="stat-item__main">
|
||
<div class="stat-item__meta">
|
||
<img
|
||
v-if="card.iconSrc"
|
||
:src="card.iconSrc"
|
||
alt=""
|
||
class="stat-item__avatar"
|
||
/>
|
||
<icon-bar-chart v-else class="stat-item__avatar-icon" />
|
||
<span class="stat-label">{{ $t(card.labelKey) }}</span>
|
||
</div>
|
||
<div class="stat-item__body">
|
||
<span class="stat-value">{{ card.value }}</span>
|
||
</div>
|
||
</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(opsCompareKey) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<p class="monitor-queue-title">{{ $t('monitor.logistics.queue.title') }}</p>
|
||
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
|
||
<a-col
|
||
v-for="card in queueStatCards"
|
||
:key="card.key"
|
||
:xs="24"
|
||
:sm="12"
|
||
:md="12"
|
||
:lg="6"
|
||
:xl="6"
|
||
>
|
||
<a-card hoverable class="stat-card" :body-style="{ padding: '12px 14px 10px', height: '100%' }">
|
||
<div class="stat-item">
|
||
<div class="stat-item__main">
|
||
<div class="stat-item__meta">
|
||
<img
|
||
v-if="card.iconSrc"
|
||
:src="card.iconSrc"
|
||
alt=""
|
||
class="stat-item__avatar"
|
||
/>
|
||
<icon-bar-chart v-else class="stat-item__avatar-icon" />
|
||
<span class="stat-label">{{ $t(card.labelKey) }}</span>
|
||
</div>
|
||
<div class="stat-item__body">
|
||
<span class="stat-value" :class="{ 'stat-value--warning': card.warning }">
|
||
{{ card.value }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-row :gutter="[16, 16]" class="logistics-monitor-trend-charts">
|
||
<a-col :xs="24" :lg="12">
|
||
<a-card :title="$t(orderTrendTitleKey)" class="chart-card">
|
||
<div ref="orderChartRef" class="chart-host"></div>
|
||
</a-card>
|
||
</a-col>
|
||
|
||
<a-col :xs="24" :lg="12">
|
||
<a-card :title="$t(revenueTrendTitleKey)" class="chart-card">
|
||
<div ref="profitChartRef" class="chart-host"></div>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
|
||
<div v-show="monitorTab === 'customer'" class="monitor-tab-panel">
|
||
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
|
||
<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 14px 10px', height: '100%' }">
|
||
<div class="stat-item">
|
||
<div class="stat-item__main">
|
||
<div class="stat-item__meta">
|
||
<icon-user-group
|
||
class="stat-item__avatar-icon"
|
||
:class="{
|
||
'is-warning': card.warning,
|
||
'is-online': card.icon === 'online',
|
||
}"
|
||
/>
|
||
<span class="stat-label">{{ $t(card.labelKey) }}</span>
|
||
</div>
|
||
<div class="stat-item__body">
|
||
<span class="stat-value" :class="{ 'stat-value--warning': card.warning }">
|
||
{{ card.value }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="card.growth.show || card.hintKey || card.extraHintKey"
|
||
class="stat-footer"
|
||
>
|
||
<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
|
||
v-if="card.hintKey"
|
||
class="stat-hint"
|
||
:class="{ 'stat-hint--accent': card.hintAccent }"
|
||
>
|
||
{{ $t(card.hintKey) }}
|
||
</div>
|
||
<div v-if="card.extraHintKey" class="stat-hint">{{ $t(card.extraHintKey) }}</div>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-row :gutter="[16, 16]" class="logistics-monitor-customer-trends">
|
||
<a-col :span="24">
|
||
<a-card class="chart-card">
|
||
<template #title>
|
||
{{ $t('monitor.logistics.chart.customerTrendsSection') }}
|
||
</template>
|
||
<div class="sign-rate-toolbar pie-toolbar">
|
||
<span class="sign-rate-toolbar__label">{{ $t('monitor.logistics.filter.line') }}</span>
|
||
<a-select
|
||
v-model="customerTrendLine"
|
||
:options="lineFilterOptions"
|
||
:style="{ width: '160px' }"
|
||
allow-clear
|
||
/>
|
||
<span class="sign-rate-toolbar__label">{{ $t('monitor.logistics.filter.statPeriod') }}</span>
|
||
<a-radio-group v-model="customerTrendPeriod" type="button" size="small">
|
||
<a-radio value="week">{{ $t('monitor.logistics.filter.periodWeek') }}</a-radio>
|
||
<a-radio value="month">{{ $t('monitor.logistics.filter.periodMonth') }}</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<a-row :gutter="[12, 12]" class="customer-trend-charts-row">
|
||
<a-col :xs="24" :lg="12">
|
||
<div class="customer-chart-block">
|
||
<div class="customer-chart-block__title">
|
||
{{ $t('monitor.logistics.chart.customerOrdersByLine') }}
|
||
</div>
|
||
<div ref="customerOrderTrendRef" class="chart-host"></div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :xs="24" :lg="12">
|
||
<div class="customer-chart-block">
|
||
<div class="customer-chart-block__title">
|
||
{{ $t('monitor.logistics.chart.customerReceivableByLine') }}
|
||
</div>
|
||
<div ref="customerReceivableTrendRef" class="chart-host"></div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
|
||
<div v-show="monitorTab === 'track'" class="monitor-tab-panel">
|
||
<a-row :gutter="[16, 16]" class="logistics-monitor-sign-rate-row">
|
||
<a-col :span="24">
|
||
<a-card class="chart-card">
|
||
<template #title>
|
||
{{ $t('monitor.logistics.chart.signRateByLine') }}
|
||
</template>
|
||
<div class="sign-rate-toolbar">
|
||
<span class="sign-rate-toolbar__label">{{ $t('monitor.logistics.filter.line') }}</span>
|
||
<a-select
|
||
v-model="filterLine"
|
||
:options="lineFilterOptions"
|
||
:style="{ width: '160px' }"
|
||
allow-clear
|
||
/>
|
||
<span class="sign-rate-toolbar__label">{{ $t('monitor.logistics.filter.signDays') }}</span>
|
||
<a-select
|
||
v-model="filterSignDays"
|
||
:options="signDaysFilterOptions"
|
||
:style="{ width: '120px' }"
|
||
/>
|
||
</div>
|
||
<div ref="signRateChartRef" class="chart-host chart-host--sign-rate"></div>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-row :gutter="[16, 16]" class="logistics-monitor-pie-row">
|
||
<a-col :span="24">
|
||
<a-card class="chart-card">
|
||
<template #title>
|
||
{{ $t('monitor.logistics.pie.sectionTitle') }}
|
||
</template>
|
||
<div class="sign-rate-toolbar pie-toolbar">
|
||
<span class="sign-rate-toolbar__label">{{ $t('monitor.logistics.filter.line') }}</span>
|
||
<a-select
|
||
v-model="pieFilterLine"
|
||
:options="lineFilterOptions"
|
||
:style="{ width: '160px' }"
|
||
allow-clear
|
||
/>
|
||
<span class="sign-rate-toolbar__label">{{ $t('monitor.logistics.filter.statPeriod') }}</span>
|
||
<a-radio-group v-model="piePeriod" type="button" size="small">
|
||
<a-radio value="week">{{ $t('monitor.logistics.filter.periodWeek') }}</a-radio>
|
||
<a-radio value="month">{{ $t('monitor.logistics.filter.periodMonth') }}</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
<a-row :gutter="[12, 12]" class="pie-charts-row">
|
||
<a-col :xs="24" :md="8">
|
||
<div class="pie-chart-block">
|
||
<div class="pie-chart-block__title">{{ $t('monitor.logistics.pie.trackComplete') }}</div>
|
||
<div ref="pieTrackCompleteRef" class="chart-host chart-host--pie"></div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<div class="pie-chart-block">
|
||
<div class="pie-chart-block__title">{{ $t('monitor.logistics.pie.trackTimely') }}</div>
|
||
<div ref="pieTrackTimelyRef" class="chart-host chart-host--pie"></div>
|
||
</div>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<div class="pie-chart-block">
|
||
<div class="pie-chart-block__title">{{ $t('monitor.logistics.pie.otd') }}</div>
|
||
<div ref="pieOtdRef" class="chart-host chart-host--pie"></div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
||
import * as echarts from 'echarts';
|
||
import { modifyAlpha } from 'zrender/lib/tool/color';
|
||
import { useI18n } from 'vue-i18n';
|
||
import {
|
||
getMonitorOverview,
|
||
formatMonitorNumber,
|
||
formatMonitorCurrency,
|
||
formatMonitorGrowth,
|
||
type MonitorDimension,
|
||
type MonitorOverview,
|
||
} from '@/api/monitor';
|
||
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||
import iconOrderNew from '@/assets/icons/workplace/order-new.svg?url';
|
||
import iconPayment from '@/assets/icons/workplace/order-payment.svg?url';
|
||
import iconGrowth from '@/assets/icons/workplace/order-growth.svg?url';
|
||
import iconPendingOnline from '@/assets/icons/workplace/order-pending-online.svg?url';
|
||
import iconProblematic from '@/assets/icons/workplace/order-problematic.svg?url';
|
||
import iconShipped from '@/assets/icons/workplace/order-shipped.svg?url';
|
||
|
||
defineOptions({ name: 'Monitor' });
|
||
|
||
const { t } = useI18n();
|
||
|
||
const monitorTabItems = computed(() => [
|
||
{ key: 'order', label: t('monitor.logistics.tab.order') },
|
||
{ key: 'customer', label: t('monitor.logistics.tab.customer') },
|
||
{ key: 'track', label: t('monitor.logistics.tab.track') },
|
||
]);
|
||
|
||
const overview = ref<MonitorOverview | null>(null);
|
||
const overviewLoading = ref(true);
|
||
const opsDimension = ref<MonitorDimension>('day');
|
||
|
||
/** silent:切换日/周/月时不整页 Spin,避免闪白 */
|
||
async function loadOverview(opts?: { silent?: boolean }) {
|
||
const silent = opts?.silent ?? false;
|
||
if (!silent) overviewLoading.value = true;
|
||
try {
|
||
overview.value = await getMonitorOverview(opsDimension.value);
|
||
} catch {
|
||
if (!silent) overview.value = null;
|
||
} finally {
|
||
if (!silent) 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 opsCompareKey = computed(() => {
|
||
if (opsDimension.value === 'week') return 'monitor.logistics.vsLastWeek';
|
||
if (opsDimension.value === 'month') return 'monitor.logistics.vsLastMonth';
|
||
return 'monitor.logistics.vsYesterday';
|
||
});
|
||
|
||
const orderTrendTitleKey = computed(() => {
|
||
if (opsDimension.value === 'week') return 'monitor.logistics.chart.ordersWeek';
|
||
if (opsDimension.value === 'month') return 'monitor.logistics.chart.ordersMonth';
|
||
return 'monitor.logistics.chart.ordersDay';
|
||
});
|
||
|
||
const revenueTrendTitleKey = computed(() => {
|
||
if (opsDimension.value === 'week') return 'monitor.logistics.chart.revenueGrossWeek';
|
||
if (opsDimension.value === 'month') return 'monitor.logistics.chart.revenueGrossMonth';
|
||
return 'monitor.logistics.chart.revenueGrossDay';
|
||
});
|
||
|
||
const opsStatCards = computed(() => {
|
||
const ops = overview.value?.ops;
|
||
const dim = opsDimension.value;
|
||
return [
|
||
{
|
||
key: 'orderCount',
|
||
labelKey: `monitor.logistics.ops.orderCount.${dim}`,
|
||
value: formatMonitorNumber(ops?.orderCount ?? 0),
|
||
growth: formatMonitorGrowth(ops?.orderCountGrowthPercent),
|
||
iconSrc: iconOrderNew,
|
||
},
|
||
{
|
||
key: 'revenue',
|
||
labelKey: `monitor.logistics.ops.revenue.${dim}`,
|
||
value: formatMonitorCurrency(ops?.revenue ?? 0),
|
||
growth: formatMonitorGrowth(ops?.revenueGrowthPercent),
|
||
iconSrc: iconPayment,
|
||
},
|
||
{
|
||
key: 'grossProfit',
|
||
labelKey: `monitor.logistics.ops.grossProfit.${dim}`,
|
||
value: formatMonitorCurrency(ops?.grossProfit ?? 0),
|
||
growth: formatMonitorGrowth(ops?.grossProfitGrowthPercent),
|
||
iconSrc: iconGrowth,
|
||
},
|
||
];
|
||
});
|
||
|
||
const queueStatCards = computed(() => {
|
||
const queue = overview.value?.queue ?? overview.value?.order;
|
||
return [
|
||
{
|
||
key: 'pendingPayment',
|
||
labelKey: 'monitor.logistics.stat.pendingPayment',
|
||
value: formatMonitorNumber(queue?.pendingPayment ?? 0),
|
||
warning: (queue?.pendingPayment ?? 0) > 0,
|
||
iconSrc: iconPayment,
|
||
},
|
||
{
|
||
key: 'pendingOnline',
|
||
labelKey: 'monitor.logistics.stat.pendingOnline',
|
||
value: formatMonitorNumber(queue?.pendingOnline ?? 0),
|
||
warning: (queue?.pendingOnline ?? 0) > 0,
|
||
iconSrc: iconPendingOnline,
|
||
},
|
||
{
|
||
key: 'pendingSign',
|
||
labelKey: 'monitor.logistics.stat.pendingSign',
|
||
value: formatMonitorNumber(queue?.pendingSign ?? 0),
|
||
warning: false,
|
||
iconSrc: iconShipped,
|
||
},
|
||
{
|
||
key: 'orderExceptions',
|
||
labelKey: 'monitor.logistics.stat.orderExceptions',
|
||
value: formatMonitorNumber(queue?.problematic ?? 0),
|
||
warning: (queue?.problematic ?? 0) > 0,
|
||
iconSrc: iconProblematic,
|
||
},
|
||
];
|
||
});
|
||
|
||
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];
|
||
|
||
const baseSignRates: Record<LineKey, number> = {
|
||
us: 93.2,
|
||
eu: 89.5,
|
||
sea: 91.0,
|
||
au: 87.3,
|
||
jp: 94.1,
|
||
uk: 90.0,
|
||
};
|
||
|
||
const orderChartRef = ref<HTMLElement | null>(null);
|
||
const profitChartRef = ref<HTMLElement | null>(null);
|
||
const signRateChartRef = ref<HTMLElement | null>(null);
|
||
const customerOrderTrendRef = ref<HTMLElement | null>(null);
|
||
const customerReceivableTrendRef = ref<HTMLElement | null>(null);
|
||
const pieTrackCompleteRef = ref<HTMLElement | null>(null);
|
||
const pieTrackTimelyRef = ref<HTMLElement | null>(null);
|
||
const pieOtdRef = ref<HTMLElement | null>(null);
|
||
|
||
const logisticsMonitorRoot = ref<HTMLElement | null>(null);
|
||
const monitorTab = ref<'customer' | 'order' | 'track'>('order');
|
||
|
||
let layoutResizeObserver: ResizeObserver | null = null;
|
||
|
||
const filterLine = ref<string | undefined>('all');
|
||
const filterSignDays = ref(7);
|
||
const pieFilterLine = ref<string | undefined>('all');
|
||
const piePeriod = ref<'week' | 'month'>('week');
|
||
const customerTrendLine = ref<string | undefined>('all');
|
||
const customerTrendPeriod = ref<'week' | 'month'>('week');
|
||
|
||
type PieMetricKey = 'complete' | 'timely' | 'otd';
|
||
|
||
const pieMetricBase: Record<LineKey, Record<PieMetricKey, number>> = {
|
||
us: { complete: 92.4, timely: 89.1, otd: 91.3 },
|
||
eu: { complete: 88.2, timely: 85.6, otd: 87.9 },
|
||
sea: { complete: 90.5, timely: 88.0, otd: 89.4 },
|
||
au: { complete: 86.8, timely: 84.2, otd: 86.1 },
|
||
jp: { complete: 93.1, timely: 90.8, otd: 92.5 },
|
||
uk: { complete: 89.4, timely: 87.0, otd: 88.6 },
|
||
};
|
||
|
||
let orderChartInstance: echarts.ECharts | null = null;
|
||
let profitChartInstance: echarts.ECharts | null = null;
|
||
let signRateChartInstance: echarts.ECharts | null = null;
|
||
let customerOrderTrendInstance: echarts.ECharts | null = null;
|
||
let customerReceivableTrendInstance: echarts.ECharts | null = null;
|
||
let pieTrackCompleteInstance: echarts.ECharts | null = null;
|
||
let pieTrackTimelyInstance: echarts.ECharts | null = null;
|
||
let pieOtdInstance: echarts.ECharts | null = null;
|
||
|
||
const lineFilterOptions = computed(() => [
|
||
{ label: t('monitor.logistics.filter.allLines'), value: 'all' },
|
||
...LINE_KEYS.map((key) => ({
|
||
label: t(`monitor.logistics.line.${key}`),
|
||
value: key,
|
||
})),
|
||
]);
|
||
|
||
const signDaysFilterOptions = computed(() =>
|
||
[3, 5, 7, 10, 15].map((n) => ({
|
||
label: t('monitor.logistics.filter.signDaysOption', { n }),
|
||
value: n,
|
||
})),
|
||
);
|
||
|
||
function buildSignRateSeries(): { categories: string[]; values: number[] } {
|
||
const lineKey = filterLine.value;
|
||
const keys: LineKey[] =
|
||
!lineKey || lineKey === 'all'
|
||
? [...LINE_KEYS]
|
||
: LINE_KEYS.includes(lineKey as LineKey)
|
||
? [lineKey as LineKey]
|
||
: [...LINE_KEYS];
|
||
|
||
const rateMap = new Map(
|
||
(overview.value?.regionSignRates ?? []).map((item) => [item.region, item.rate]),
|
||
);
|
||
|
||
const categories: string[] = [];
|
||
const values: number[] = [];
|
||
|
||
keys.forEach((key) => {
|
||
categories.push(t(`monitor.logistics.line.${key}`));
|
||
values.push(Number(rateMap.get(key) ?? 0));
|
||
});
|
||
|
||
return { categories, values };
|
||
}
|
||
|
||
function renderSignRateChart() {
|
||
if (!signRateChartRef.value) return;
|
||
const primary = readCssVar('--app-primary-color', '#2D8C8C');
|
||
const axisLine = readCssVar('--color-border-2', '#e5e6eb');
|
||
const { categories, values } = buildSignRateSeries();
|
||
|
||
if (!signRateChartInstance) {
|
||
signRateChartInstance = echarts.init(signRateChartRef.value);
|
||
}
|
||
|
||
signRateChartInstance.setOption({
|
||
grid: { left: '3%', right: '3%', bottom: '3%', top: '8%', containLabel: true },
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'shadow' },
|
||
valueFormatter: (val: number) => `${val}%`,
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: categories,
|
||
axisTick: { alignWithLabel: true },
|
||
axisLine: { lineStyle: { color: axisLine } },
|
||
axisLabel: { interval: 0, rotate: categories.length > 6 ? 24 : 0 },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
min: 0,
|
||
max: 100,
|
||
axisLabel: { formatter: '{value}%' },
|
||
axisLine: { lineStyle: { color: axisLine } },
|
||
splitLine: { lineStyle: { color: axisLine, opacity: 0.35 } },
|
||
name: t('monitor.logistics.axis.complianceRate'),
|
||
nameTextStyle: { color: readCssVar('--color-text-2', '#86909c'), fontSize: 12 },
|
||
},
|
||
series: [
|
||
{
|
||
name: t('monitor.logistics.series.signRate'),
|
||
type: 'bar',
|
||
barMaxWidth: 48,
|
||
data: values,
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: modifyAlpha(primary, 0.95) },
|
||
{ offset: 1, color: modifyAlpha(primary, 0.45) },
|
||
]),
|
||
borderRadius: [4, 4, 0, 0],
|
||
},
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
formatter: (p: { value: number }) => `${p.value}%`,
|
||
fontSize: 11,
|
||
color: readCssVar('--color-text-2', '#86909c'),
|
||
},
|
||
},
|
||
],
|
||
});
|
||
}
|
||
|
||
function getActiveLineKeysForPie(): LineKey[] {
|
||
const v = pieFilterLine.value;
|
||
if (!v || v === 'all') return [...LINE_KEYS];
|
||
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 pass = getPiePassRate(metric);
|
||
return Math.min(99.5, Math.max(0, Number(pass.toFixed(1))));
|
||
}
|
||
|
||
function buildPieChartOption(passColor: string, metric: PieMetricKey) {
|
||
const pass = averagePieMetric(metric);
|
||
const fail = Number((100 - pass).toFixed(1));
|
||
const neutral = readCssVar('--color-fill-3', '#e5e6eb');
|
||
const passName = t('monitor.logistics.pie.pass');
|
||
const failName = t('monitor.logistics.pie.fail');
|
||
|
||
return {
|
||
tooltip: {
|
||
trigger: 'item' as const,
|
||
formatter: `{b}: {c}% ({d}%)`,
|
||
},
|
||
legend: {
|
||
bottom: 0,
|
||
icon: 'circle',
|
||
textStyle: { fontSize: 12, color: readCssVar('--color-text-2', '#86909c') },
|
||
},
|
||
series: [
|
||
{
|
||
type: 'pie' as const,
|
||
radius: ['44%', '72%'],
|
||
center: ['50%', '46%'],
|
||
avoidLabelOverlap: true,
|
||
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
||
label: {
|
||
formatter: '{b}\n{d}%',
|
||
fontSize: 11,
|
||
color: readCssVar('--color-text-2', '#86909c'),
|
||
},
|
||
labelLine: { length: 10, length2: 8 },
|
||
data: [
|
||
{ name: passName, value: pass, itemStyle: { color: passColor } },
|
||
{ name: failName, value: fail, itemStyle: { color: neutral } },
|
||
],
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
function renderPieCharts() {
|
||
const primary = readCssVar('--app-primary-color', '#2D8C8C');
|
||
const timelyColor = readCssVar('--color-link-6', '#14C9C9');
|
||
const otdColor = readCssVar('--color-warning-6', '#FF7D00');
|
||
|
||
if (pieTrackCompleteRef.value) {
|
||
if (!pieTrackCompleteInstance) pieTrackCompleteInstance = echarts.init(pieTrackCompleteRef.value);
|
||
pieTrackCompleteInstance.setOption(buildPieChartOption(primary, 'complete'));
|
||
}
|
||
if (pieTrackTimelyRef.value) {
|
||
if (!pieTrackTimelyInstance) pieTrackTimelyInstance = echarts.init(pieTrackTimelyRef.value);
|
||
pieTrackTimelyInstance.setOption(buildPieChartOption(timelyColor, 'timely'));
|
||
}
|
||
if (pieOtdRef.value) {
|
||
if (!pieOtdInstance) pieOtdInstance = echarts.init(pieOtdRef.value);
|
||
pieOtdInstance.setOption(buildPieChartOption(otdColor, 'otd'));
|
||
}
|
||
}
|
||
|
||
function readCssVar(name: string, fallback: string): string {
|
||
if (typeof document === 'undefined') return fallback;
|
||
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||
return v || fallback;
|
||
}
|
||
|
||
function onResize() {
|
||
orderChartInstance?.resize();
|
||
profitChartInstance?.resize();
|
||
signRateChartInstance?.resize();
|
||
customerOrderTrendInstance?.resize();
|
||
customerReceivableTrendInstance?.resize();
|
||
pieTrackCompleteInstance?.resize();
|
||
pieTrackTimelyInstance?.resize();
|
||
pieOtdInstance?.resize();
|
||
}
|
||
|
||
function getLineTrendColors(): string[] {
|
||
const primary = readCssVar('--app-primary-color', '#2D8C8C');
|
||
return [primary, '#00B42A', '#FF7D00', '#14C9C9', '#722ED1', '#F5319D'];
|
||
}
|
||
|
||
function getActiveCustomerLineKeys(): LineKey[] {
|
||
const v = customerTrendLine.value;
|
||
if (!v || v === 'all') return [...LINE_KEYS];
|
||
return LINE_KEYS.includes(v as LineKey) ? [v as LineKey] : [...LINE_KEYS];
|
||
}
|
||
|
||
function buildCustomerPeriodLabels(period: 'week' | 'month'): string[] {
|
||
const n = period === 'week' ? 8 : 6;
|
||
return Array.from({ length: n }, (_, i) =>
|
||
period === 'week'
|
||
? t('monitor.logistics.axis.weekIndex', { n: i + 1 })
|
||
: t('monitor.logistics.axis.monthIndex', { n: i + 1 }),
|
||
);
|
||
}
|
||
|
||
const orderBaseByLine: Record<LineKey, number> = {
|
||
us: 420,
|
||
eu: 310,
|
||
sea: 280,
|
||
au: 190,
|
||
jp: 350,
|
||
uk: 260,
|
||
};
|
||
|
||
function buildMockOrderSeries(line: LineKey, period: 'week' | 'month', len: number): number[] {
|
||
const scale = period === 'month' ? 4.2 : 1;
|
||
const b = orderBaseByLine[line] * scale;
|
||
const idx = LINE_KEYS.indexOf(line);
|
||
return Array.from({ length: len }, (_, i) => {
|
||
const wave = Math.sin((i + idx) * 0.55) * (period === 'month' ? 45 : 12);
|
||
const trend = i * (period === 'month' ? 8 : 3);
|
||
return Math.round(b + wave + trend);
|
||
});
|
||
}
|
||
|
||
function buildMockReceivableSeries(line: LineKey, period: 'week' | 'month', len: number): number[] {
|
||
const orders = buildMockOrderSeries(line, period, len);
|
||
const unitPrice = 280 + LINE_KEYS.indexOf(line) * 15;
|
||
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 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) => {
|
||
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) => {
|
||
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 };
|
||
|
||
const baseOption = {
|
||
grid,
|
||
legend: {
|
||
show: keys.length > 1,
|
||
type: 'scroll' as const,
|
||
bottom: 0,
|
||
textStyle: { fontSize: 11, color: text2 },
|
||
},
|
||
xAxis: {
|
||
type: 'category' as const,
|
||
data: categories,
|
||
axisLine: { lineStyle: { color: axisLine } },
|
||
axisLabel: { color: text2, interval: 0, rotate: categories.length > 7 ? 18 : 0 },
|
||
},
|
||
yAxis: {
|
||
type: 'value' as const,
|
||
axisLine: { lineStyle: { color: axisLine } },
|
||
splitLine: { lineStyle: { color: axisLine, opacity: 0.35 } },
|
||
axisLabel: { color: text2 },
|
||
},
|
||
};
|
||
|
||
if (customerOrderTrendRef.value) {
|
||
if (!customerOrderTrendInstance) {
|
||
customerOrderTrendInstance = echarts.init(customerOrderTrendRef.value);
|
||
}
|
||
customerOrderTrendInstance.setOption(
|
||
{
|
||
...baseOption,
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
},
|
||
series: orderSeries,
|
||
},
|
||
{ notMerge: true },
|
||
);
|
||
}
|
||
|
||
if (customerReceivableTrendRef.value) {
|
||
if (!customerReceivableTrendInstance) {
|
||
customerReceivableTrendInstance = echarts.init(customerReceivableTrendRef.value);
|
||
}
|
||
customerReceivableTrendInstance.setOption(
|
||
{
|
||
...baseOption,
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
valueFormatter: (val: number) => `¥ ${Number(val).toLocaleString()}`,
|
||
},
|
||
yAxis: {
|
||
...baseOption.yAxis,
|
||
axisLabel: {
|
||
color: text2,
|
||
formatter: (v: number) => `¥${(v / 1000).toFixed(0)}k`,
|
||
},
|
||
},
|
||
series: receivableSeries,
|
||
},
|
||
{ notMerge: true },
|
||
);
|
||
}
|
||
}
|
||
|
||
function ensureCustomerCharts() {
|
||
renderCustomerTrendCharts();
|
||
customerOrderTrendInstance?.resize();
|
||
customerReceivableTrendInstance?.resize();
|
||
}
|
||
|
||
function ensureOrderLineCharts() {
|
||
const primary = readCssVar('--app-primary-color', '#2D8C8C');
|
||
const axisLine = readCssVar('--color-border-2', '#e5e6eb');
|
||
const orderTrend = overview.value?.orderTrend ?? overview.value?.orderTrend7d ?? [];
|
||
const revenueTrend = overview.value?.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(
|
||
{
|
||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||
tooltip: { trigger: 'axis' },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: dayLabels.length ? dayLabels : [t('workplace.record.nodata')],
|
||
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: orderValues,
|
||
itemStyle: { color: primary },
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: modifyAlpha(primary, 0.2) },
|
||
{ offset: 1, color: modifyAlpha(primary, 0) },
|
||
]),
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{ notMerge: true },
|
||
);
|
||
}
|
||
|
||
if (profitChartRef.value) {
|
||
if (!profitChartInstance) {
|
||
profitChartInstance = echarts.init(profitChartRef.value);
|
||
}
|
||
const revenueColor = '#00B42A';
|
||
const grossColor = '#FF7D00';
|
||
profitChartInstance.setOption(
|
||
{
|
||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||
tooltip: { trigger: 'axis' },
|
||
legend: {
|
||
data: [t('monitor.logistics.series.revenue'), t('monitor.logistics.series.grossProfit')],
|
||
bottom: 0,
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: dayLabels.length ? dayLabels : [t('workplace.record.nodata')],
|
||
axisLine: { lineStyle: { color: axisLine } },
|
||
},
|
||
yAxis: { type: 'value', axisLine: { lineStyle: { color: axisLine } } },
|
||
series: [
|
||
{
|
||
name: t('monitor.logistics.series.revenue'),
|
||
type: 'bar',
|
||
barMaxWidth: 28,
|
||
data: revenueValues,
|
||
itemStyle: { color: revenueColor, borderRadius: [4, 4, 0, 0] },
|
||
},
|
||
{
|
||
name: t('monitor.logistics.series.grossProfit'),
|
||
type: 'bar',
|
||
barMaxWidth: 28,
|
||
data: grossValues,
|
||
itemStyle: { color: grossColor, borderRadius: [4, 4, 0, 0] },
|
||
},
|
||
],
|
||
},
|
||
{ notMerge: true },
|
||
);
|
||
}
|
||
|
||
orderChartInstance?.resize();
|
||
profitChartInstance?.resize();
|
||
}
|
||
|
||
function ensureTrackCharts() {
|
||
renderSignRateChart();
|
||
renderPieCharts();
|
||
signRateChartInstance?.resize();
|
||
pieTrackCompleteInstance?.resize();
|
||
pieTrackTimelyInstance?.resize();
|
||
pieOtdInstance?.resize();
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await nextTick();
|
||
window.addEventListener('resize', onResize);
|
||
if (typeof ResizeObserver !== 'undefined' && logisticsMonitorRoot.value) {
|
||
layoutResizeObserver = new ResizeObserver(() => onResize());
|
||
layoutResizeObserver.observe(logisticsMonitorRoot.value);
|
||
}
|
||
await loadOverview();
|
||
});
|
||
|
||
function disposeOrderCharts() {
|
||
orderChartInstance?.dispose();
|
||
profitChartInstance?.dispose();
|
||
orderChartInstance = null;
|
||
profitChartInstance = null;
|
||
}
|
||
|
||
function disposeTrackCharts() {
|
||
signRateChartInstance?.dispose();
|
||
pieTrackCompleteInstance?.dispose();
|
||
pieTrackTimelyInstance?.dispose();
|
||
pieOtdInstance?.dispose();
|
||
signRateChartInstance = null;
|
||
pieTrackCompleteInstance = null;
|
||
pieTrackTimelyInstance = null;
|
||
pieOtdInstance = null;
|
||
}
|
||
|
||
function disposeCustomerCharts() {
|
||
customerOrderTrendInstance?.dispose();
|
||
customerReceivableTrendInstance?.dispose();
|
||
customerOrderTrendInstance = null;
|
||
customerReceivableTrendInstance = null;
|
||
}
|
||
|
||
watch(
|
||
monitorTab,
|
||
(key) => {
|
||
// 面板用 v-show 常驻,切 Tab 只 resize / 补画,不 dispose,避免闪一下
|
||
nextTick(() => {
|
||
if (key === 'order') ensureOrderLineCharts();
|
||
if (key === 'track') ensureTrackCharts();
|
||
if (key === 'customer') ensureCustomerCharts();
|
||
nextTick(() => onResize());
|
||
});
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
watch(opsDimension, () => {
|
||
void loadOverview({ silent: true });
|
||
});
|
||
|
||
watch([filterLine, filterSignDays], () => {
|
||
nextTick(() => {
|
||
if (monitorTab.value === 'track') renderSignRateChart();
|
||
});
|
||
});
|
||
|
||
watch([pieFilterLine, piePeriod], () => {
|
||
nextTick(() => {
|
||
if (monitorTab.value === 'track') renderPieCharts();
|
||
});
|
||
});
|
||
|
||
watch([customerTrendLine, customerTrendPeriod], () => {
|
||
nextTick(() => {
|
||
if (monitorTab.value === 'customer') renderCustomerTrendCharts();
|
||
});
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
layoutResizeObserver?.disconnect();
|
||
layoutResizeObserver = null;
|
||
window.removeEventListener('resize', onResize);
|
||
disposeOrderCharts();
|
||
disposeTrackCharts();
|
||
disposeCustomerCharts();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.logistics-monitor-container {
|
||
--lm-well-bg: #ffffff;
|
||
--lm-well-shadow:
|
||
inset 0 0 0 1px rgba(15, 23, 42, 0.06),
|
||
inset 0 1px 4px rgba(15, 23, 42, 0.05);
|
||
--lm-well-shadow-deep:
|
||
inset 0 0 0 1px rgba(15, 23, 42, 0.08),
|
||
inset 0 2px 6px rgba(15, 23, 42, 0.07);
|
||
|
||
box-sizing: border-box;
|
||
flex: 1;
|
||
min-height: 0;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 16px;
|
||
background: var(--app-page-bg);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.monitor-topbar {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 12px 16px;
|
||
flex-shrink: 0;
|
||
margin-bottom: 12px;
|
||
min-height: 36px;
|
||
}
|
||
|
||
.monitor-topbar__spacer {
|
||
flex: 1 1 auto;
|
||
min-width: 8px;
|
||
}
|
||
|
||
.logistics-monitor-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
line-height: 32px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.monitor-top-tabs {
|
||
flex-shrink: 0;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.monitor-ops-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.monitor-ops-toolbar__label {
|
||
font-size: 13px;
|
||
color: var(--color-text-2);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.logistics-monitor-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
|
||
:deep(.arco-spin) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
max-height: 100%;
|
||
}
|
||
|
||
:deep(.arco-spin-children) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
.monitor-shell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.monitor-tab-panels {
|
||
flex: 1;
|
||
min-height: 0;
|
||
min-width: 0;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.monitor-tab-panel {
|
||
min-width: 0;
|
||
}
|
||
|
||
.monitor-queue-title {
|
||
margin: 16px 0 10px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
}
|
||
|
||
.logistics-monitor-stats {
|
||
margin-bottom: 10px;
|
||
|
||
:deep(.arco-col) {
|
||
display: flex;
|
||
min-width: 0;
|
||
}
|
||
|
||
}
|
||
|
||
.logistics-monitor-trend-charts {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.logistics-monitor-customer-trends {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.customer-trend-charts-row {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.customer-chart-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
width: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.customer-chart-block__title {
|
||
text-align: center;
|
||
font-size: clamp(13px, 1.5vw, 14px);
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
margin-bottom: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.logistics-monitor-sign-rate-row {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.sign-rate-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.sign-rate-toolbar__label {
|
||
font-size: 14px;
|
||
color: var(--color-text-2);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
width: 100%;
|
||
min-height: 84px;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: none !important;
|
||
border-radius: 14px !important;
|
||
background: var(--lm-well-bg, #ffffff) !important;
|
||
box-shadow: var(--lm-well-shadow) !important;
|
||
transition: box-shadow 0.2s ease;
|
||
|
||
&:hover {
|
||
background: var(--lm-well-bg, #ffffff) !important;
|
||
box-shadow: var(--lm-well-shadow-deep) !important;
|
||
}
|
||
|
||
:deep(.arco-card-body) {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
/* 对齐工作台:左 icon+标题竖排,右数值;icon 无底框 */
|
||
.stat-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-height: 0;
|
||
}
|
||
|
||
.stat-item__main {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.stat-item__meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.stat-item__avatar {
|
||
display: block;
|
||
width: 40px;
|
||
height: 40px;
|
||
object-fit: contain;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.stat-item__avatar-icon {
|
||
display: inline-flex;
|
||
width: 40px;
|
||
height: 40px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22px;
|
||
color: var(--app-primary-color);
|
||
background: transparent;
|
||
|
||
&.is-warning {
|
||
color: rgb(var(--orange-6));
|
||
}
|
||
|
||
&.is-online {
|
||
color: rgb(var(--cyan-6));
|
||
}
|
||
}
|
||
|
||
.stat-item__body {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: flex-end;
|
||
flex-shrink: 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
.stat-label {
|
||
max-width: 100%;
|
||
font-size: 12px;
|
||
line-height: 1.3;
|
||
color: var(--color-text-3);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1;
|
||
letter-spacing: -0.02em;
|
||
color: var(--color-text-1);
|
||
word-break: break-all;
|
||
}
|
||
|
||
.stat-value--warning {
|
||
color: rgb(var(--orange-6));
|
||
}
|
||
|
||
.stat-value--danger {
|
||
color: rgb(var(--red-6));
|
||
}
|
||
|
||
.stat-footer {
|
||
margin-top: auto;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
justify-content: flex-end;
|
||
gap: 2px;
|
||
text-align: right;
|
||
}
|
||
|
||
.stat-trend {
|
||
font-size: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stat-trend.up {
|
||
color: rgb(var(--green-6));
|
||
}
|
||
|
||
.stat-trend.down {
|
||
color: rgb(var(--red-6));
|
||
}
|
||
|
||
.stat-hint {
|
||
font-size: 12px;
|
||
line-height: 1.35;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.stat-hint--accent {
|
||
color: var(--app-primary-color);
|
||
}
|
||
|
||
.chart-card {
|
||
height: auto;
|
||
width: 100%;
|
||
border: none !important;
|
||
border-radius: 12px !important;
|
||
background: var(--lm-well-bg, #ffffff) !important;
|
||
box-shadow: var(--lm-well-shadow, inset 0 2px 5px rgba(15, 23, 42, 0.08)) !important;
|
||
|
||
:deep(.arco-card-header),
|
||
:deep(.arco-card-body) {
|
||
background: transparent;
|
||
border-color: transparent;
|
||
}
|
||
}
|
||
|
||
.chart-host {
|
||
width: 100%;
|
||
height: clamp(200px, min(32vh, 36vw), 380px);
|
||
min-height: 200px;
|
||
max-height: 420px;
|
||
}
|
||
|
||
.chart-host--sign-rate {
|
||
height: clamp(220px, min(34vh, 38vw), 400px);
|
||
min-height: 220px;
|
||
max-height: 440px;
|
||
}
|
||
|
||
.logistics-monitor-pie-row {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.pie-toolbar {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pie-charts-row {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.pie-chart-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
width: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.pie-chart-block__title {
|
||
text-align: center;
|
||
font-size: clamp(13px, 1.5vw, 14px);
|
||
font-weight: 600;
|
||
color: var(--color-text-1);
|
||
margin-bottom: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.chart-host--pie {
|
||
width: 100%;
|
||
height: clamp(200px, min(28vh, 32vw), 340px);
|
||
min-height: 200px;
|
||
max-height: 380px;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.chart-host {
|
||
max-height: none;
|
||
height: clamp(200px, 40vw, 280px);
|
||
}
|
||
|
||
.chart-host--sign-rate {
|
||
max-height: none;
|
||
height: clamp(210px, 42vw, 300px);
|
||
}
|
||
|
||
.chart-host--pie {
|
||
max-height: none;
|
||
height: clamp(190px, 55vw, 260px);
|
||
}
|
||
}
|
||
</style>
|