All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m56s
- 支付弹窗(PC/H5)支持选券与批量支付体验优化,运单管理复用我的订单支付弹窗;立即支付仅允许待支付订单 - 订单详情费用明细展示优惠券抵扣/会员折扣负数行与「含优惠」;价格配置与手工加费过滤抵扣费用项 - 申报明细新增产品标识编码/非标产品标识编码/标准产品标识编码(PC/H5 下单、改单、详情、开放接口文档、公式目录、Excel 模板) - 9610 页「批量申报」改为「创建9610订单」:选运单并补充出口资质备案、电商平台 - 店铺订单批量按钮按动作独立 loading,获取面单限并发并显示进度 - 另含:国家管理、登录门户切换、监控与工作台、公告弹窗、nginx/vite 配置等改动
1430 lines
45 KiB
Vue
1430 lines
45 KiB
Vue
<template>
|
|
<div ref="logisticsMonitorRoot" class="logistics-monitor-container">
|
|
<div class="logistics-monitor-card">
|
|
<div class="logistics-monitor-header">
|
|
<h2 class="logistics-monitor-title">{{ $t('monitor.logistics.pageTitle') }}</h2>
|
|
</div>
|
|
|
|
<div class="logistics-monitor-body">
|
|
<a-spin :loading="overviewLoading" style="width: 100%">
|
|
<a-tabs
|
|
v-model:active-key="monitorTab"
|
|
type="rounded"
|
|
:animation="false"
|
|
:justify="true"
|
|
class="monitor-top-tabs"
|
|
>
|
|
<a-tab-pane key="order" :title="$t('monitor.logistics.tab.order')">
|
|
<div 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>
|
|
|
|
<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', height: '100%' }">
|
|
<div class="stat-item">
|
|
<div class="stat-item__head">
|
|
<div class="stat-label">{{ $t(card.labelKey) }}</div>
|
|
<div class="stat-item__icon stat-item__icon--primary">
|
|
<img v-if="card.iconSrc" :src="card.iconSrc" alt="" class="stat-item__icon-img" />
|
|
<icon-bar-chart v-else />
|
|
</div>
|
|
</div>
|
|
<div class="stat-value">{{ 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(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', height: '100%' }">
|
|
<div class="stat-item">
|
|
<div class="stat-item__head">
|
|
<div class="stat-label">{{ $t(card.labelKey) }}</div>
|
|
<div
|
|
class="stat-item__icon"
|
|
:class="card.warning ? 'stat-item__icon--warning' : 'stat-item__icon--primary'"
|
|
>
|
|
<img v-if="card.iconSrc" :src="card.iconSrc" alt="" class="stat-item__icon-img" />
|
|
<icon-bar-chart v-else />
|
|
</div>
|
|
</div>
|
|
<div class="stat-value" :class="{ 'stat-value--warning': card.warning }">
|
|
{{ card.value }}
|
|
</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>
|
|
</a-tab-pane>
|
|
|
|
<a-tab-pane key="customer" :title="$t('monitor.logistics.tab.customer')">
|
|
<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', height: '100%' }">
|
|
<div class="stat-item">
|
|
<div class="stat-item__head">
|
|
<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" :class="{ 'stat-value--warning': card.warning }">{{ card.value }}</div>
|
|
<div 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>
|
|
</a-tab-pane>
|
|
|
|
<a-tab-pane key="track" :title="$t('monitor.logistics.tab.track')">
|
|
<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>
|
|
</a-tab-pane>
|
|
</a-tabs>
|
|
</a-spin>
|
|
</div>
|
|
</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 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 overview = ref<MonitorOverview | null>(null);
|
|
const overviewLoading = ref(true);
|
|
const opsDimension = ref<MonitorDimension>('day');
|
|
|
|
async function loadOverview() {
|
|
overviewLoading.value = true;
|
|
try {
|
|
overview.value = await getMonitorOverview(opsDimension.value);
|
|
} 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 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: 'line',
|
|
smooth: true,
|
|
data: revenueValues,
|
|
itemStyle: { color: revenueColor },
|
|
},
|
|
{
|
|
name: t('monitor.logistics.series.grossProfit'),
|
|
type: 'line',
|
|
smooth: true,
|
|
data: grossValues,
|
|
itemStyle: { color: grossColor },
|
|
},
|
|
],
|
|
},
|
|
{ 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, prev) => {
|
|
if (prev === 'order') disposeOrderCharts();
|
|
if (prev === 'track') disposeTrackCharts();
|
|
if (prev === 'customer') disposeCustomerCharts();
|
|
nextTick(() => {
|
|
if (key === 'order') ensureOrderLineCharts();
|
|
if (key === 'track') ensureTrackCharts();
|
|
if (key === 'customer') ensureCustomerCharts();
|
|
nextTick(() => onResize());
|
|
});
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
watch(opsDimension, () => {
|
|
void loadOverview();
|
|
});
|
|
|
|
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);
|
|
orderChartInstance?.dispose();
|
|
profitChartInstance?.dispose();
|
|
signRateChartInstance?.dispose();
|
|
customerOrderTrendInstance?.dispose();
|
|
customerReceivableTrendInstance?.dispose();
|
|
pieTrackCompleteInstance?.dispose();
|
|
pieTrackTimelyInstance?.dispose();
|
|
pieOtdInstance?.dispose();
|
|
orderChartInstance = null;
|
|
profitChartInstance = null;
|
|
signRateChartInstance = null;
|
|
customerOrderTrendInstance = null;
|
|
customerReceivableTrendInstance = null;
|
|
pieTrackCompleteInstance = null;
|
|
pieTrackTimelyInstance = null;
|
|
pieOtdInstance = null;
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.logistics-monitor-container {
|
|
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;
|
|
}
|
|
|
|
.logistics-monitor-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
flex: 1;
|
|
min-height: 0;
|
|
height: 100%;
|
|
max-height: 100%;
|
|
background: var(--app-card-bg);
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
padding: 16px;
|
|
box-sizing: border-box;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.logistics-monitor-header {
|
|
margin-bottom: 16px;
|
|
flex-shrink: 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.logistics-monitor-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.monitor-top-tabs {
|
|
width: 100%;
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
|
|
:deep(.arco-tabs) {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.arco-tabs-nav) {
|
|
justify-content: flex-start;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
:deep(.arco-tabs-nav-tab-list) {
|
|
justify-content: flex-start;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
:deep(.arco-tabs-content) {
|
|
flex: 1;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
padding: 12px 0 0;
|
|
box-sizing: border-box;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
:deep(.arco-tabs-content-list) {
|
|
height: 100%;
|
|
}
|
|
|
|
:deep(.arco-tabs-pane) {
|
|
overflow: visible;
|
|
}
|
|
}
|
|
|
|
.logistics-monitor-title {
|
|
margin: 0;
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: var(--color-text-1);
|
|
}
|
|
|
|
.monitor-ops-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.monitor-ops-toolbar__label {
|
|
font-size: 13px;
|
|
color: var(--color-text-2);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.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: 0;
|
|
|
|
:deep(.arco-card) {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
:deep(.arco-card-body) {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-sizing: border-box;
|
|
}
|
|
}
|
|
|
|
.stat-item {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.stat-item__head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
margin-bottom: 4px;
|
|
min-height: 40px;
|
|
}
|
|
|
|
.stat-item__icon {
|
|
flex-shrink: 0;
|
|
margin-top: -2px;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.stat-item__icon-img {
|
|
width: 28px;
|
|
height: 28px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.stat-item__icon--primary {
|
|
background: var(--app-primary-color-light);
|
|
color: var(--app-primary-color);
|
|
}
|
|
|
|
.stat-item__icon--warning {
|
|
background: rgb(var(--orange-1));
|
|
color: rgb(var(--orange-6));
|
|
}
|
|
|
|
.stat-item__icon--danger {
|
|
background: rgb(var(--red-1));
|
|
color: rgb(var(--red-6));
|
|
}
|
|
|
|
.stat-item__icon--online {
|
|
background: rgb(var(--cyan-1));
|
|
color: rgb(var(--cyan-6));
|
|
}
|
|
|
|
.stat-label {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 13px;
|
|
line-height: 1.35;
|
|
color: var(--color-text-2);
|
|
padding-top: 2px;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: clamp(18px, 2.4vw, 22px);
|
|
font-weight: 600;
|
|
line-height: 1.25;
|
|
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;
|
|
justify-content: flex-end;
|
|
gap: 4px;
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.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%;
|
|
}
|
|
|
|
.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>
|