|
|
|
|
@ -0,0 +1,653 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div ref="dashboardRef" class="sales-contract-dashboard" :class="{ 'is-fullscreen': isFullscreen }">
|
|
|
|
|
<header class="dashboard-header">
|
|
|
|
|
<div class="dashboard-header__filter">
|
|
|
|
|
<span class="dashboard-header__filter-label">签订时间</span>
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="dateRange"
|
|
|
|
|
type="daterange"
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
range-separator="-"
|
|
|
|
|
start-placeholder="开始日期"
|
|
|
|
|
end-placeholder="结束日期"
|
|
|
|
|
:clearable="false"
|
|
|
|
|
class="dashboard-header__date-picker"
|
|
|
|
|
@change="handleDateChange"
|
|
|
|
|
/>
|
|
|
|
|
<el-button type="primary" plain icon="Search" v-hasPermi="['oa/erp:salesContractDashboard:query']" @click="handleQuery">查询</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<h1 class="dashboard-header__title">销售合同仪表盘</h1>
|
|
|
|
|
<div class="dashboard-header__actions">
|
|
|
|
|
<el-button link type="primary" v-hasPermi="['oa/erp:salesContractDashboard:query']" @click="handleRefresh">
|
|
|
|
|
<el-icon><Refresh /></el-icon>
|
|
|
|
|
刷新
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button link type="primary" @click="toggleFullscreen">
|
|
|
|
|
<el-icon><component :is="isFullscreen ? Close : FullScreen" /></el-icon>
|
|
|
|
|
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div v-loading="loading" class="dashboard-body">
|
|
|
|
|
<!-- 顶部指标区 -->
|
|
|
|
|
<section class="metrics-row">
|
|
|
|
|
<div class="metric-card metric-card--summary">
|
|
|
|
|
<div class="metric-card__accent" />
|
|
|
|
|
<div class="metric-card__content">
|
|
|
|
|
<div class="metric-card__value-wrap">
|
|
|
|
|
<span class="metric-card__value">{{ formatAmountNumber(dashboardData.totalTransactionAmount) }}</span>
|
|
|
|
|
<span class="metric-card__unit">元</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="metric-card__label">合同成交总额</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="metric-card metric-card--summary">
|
|
|
|
|
<div class="metric-card__accent" />
|
|
|
|
|
<div class="metric-card__content">
|
|
|
|
|
<div class="metric-card__value-wrap">
|
|
|
|
|
<span class="metric-card__value">{{ formatAmountNumber(dashboardData.weeklyNewContractAmount) }}</span>
|
|
|
|
|
<span class="metric-card__unit">元</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="metric-card__label">本周新签合同额</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="metric-card metric-card--chart">
|
|
|
|
|
<div class="metric-card__chart-header">
|
|
|
|
|
<span class="metric-card__chart-title">业务方向销售额比例</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div ref="ratioChartRef" class="metric-card__chart" />
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- 底部图表区 -->
|
|
|
|
|
<section class="charts-row">
|
|
|
|
|
<div class="chart-panel">
|
|
|
|
|
<div class="chart-panel__header">
|
|
|
|
|
<span class="chart-panel__title">成交额客户分布</span>
|
|
|
|
|
<span class="chart-panel__unit">单位:元</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div ref="customerChartRef" class="chart-panel__body chart-panel__body--customer" :style="{ minHeight: `${customerChartHeight}px` }" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chart-panel">
|
|
|
|
|
<div class="chart-panel__header">
|
|
|
|
|
<span class="chart-panel__title">销售合同额排行榜</span>
|
|
|
|
|
<span class="chart-panel__unit">单位:元</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div ref="rankingChartRef" class="chart-panel__body" />
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts" name="SalesContractDashboard">
|
|
|
|
|
import { Close, FullScreen, Refresh } from '@element-plus/icons-vue';
|
|
|
|
|
import { useResizeObserver } from '@vueuse/core';
|
|
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
import type { ECharts } from 'echarts';
|
|
|
|
|
import { getSalesContractDashboard } from '@/api/oa/erp/salesContractDashboard';
|
|
|
|
|
import type { SalesContractDashboardVo } from '@/api/oa/erp/salesContractDashboard/types';
|
|
|
|
|
import { checkPermi } from '@/utils/permission';
|
|
|
|
|
|
|
|
|
|
const DASHBOARD_QUERY_PERM = ['oa/erp:salesContractDashboard:query'];
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
|
|
|
|
|
const createEmptyDashboard = (): SalesContractDashboardVo => ({
|
|
|
|
|
totalTransactionAmount: 0,
|
|
|
|
|
weeklyNewContractAmount: 0,
|
|
|
|
|
businessDirectionRatios: [],
|
|
|
|
|
customerTransactionDistribution: [],
|
|
|
|
|
salesContractRanking: []
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getCurrentYearRange = (): [string, string] => {
|
|
|
|
|
const year = new Date().getFullYear();
|
|
|
|
|
return [`${year}-01-01`, `${year}-12-31`];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const dashboardRef = ref<HTMLElement>();
|
|
|
|
|
const ratioChartRef = ref<HTMLElement>();
|
|
|
|
|
const customerChartRef = ref<HTMLElement>();
|
|
|
|
|
const rankingChartRef = ref<HTMLElement>();
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const dateRange = ref<[string, string]>(getCurrentYearRange());
|
|
|
|
|
const dashboardData = ref<SalesContractDashboardVo>(createEmptyDashboard());
|
|
|
|
|
|
|
|
|
|
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(dashboardRef);
|
|
|
|
|
|
|
|
|
|
let ratioChart: ECharts | null = null;
|
|
|
|
|
let customerChart: ECharts | null = null;
|
|
|
|
|
let rankingChart: ECharts | null = null;
|
|
|
|
|
|
|
|
|
|
const CHART_COLORS = {
|
|
|
|
|
primary: '#3B76F1',
|
|
|
|
|
ranking: '#B37FEB',
|
|
|
|
|
/** 业务方向饼图配色(多色系、易区分) */
|
|
|
|
|
ratioPalette: [
|
|
|
|
|
'#3B76F1',
|
|
|
|
|
'#5AD8A6',
|
|
|
|
|
'#F6BD16',
|
|
|
|
|
'#E86452',
|
|
|
|
|
'#6DC8EC',
|
|
|
|
|
'#945FB9',
|
|
|
|
|
'#FF9845',
|
|
|
|
|
'#1E9493',
|
|
|
|
|
'#FF99C3',
|
|
|
|
|
'#269A99',
|
|
|
|
|
'#7585A2',
|
|
|
|
|
'#F08BB4'
|
|
|
|
|
],
|
|
|
|
|
palette: ['#3B76F1', '#5B8FF9', '#69C0FF', '#91D5FF', '#ADC6FF']
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const AMOUNT_UNIT = '元';
|
|
|
|
|
|
|
|
|
|
const formatAmountNumber = (amount: number) => {
|
|
|
|
|
return Number(amount || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatAmount = (amount: number) => `${formatAmountNumber(amount)} ${AMOUNT_UNIT}`;
|
|
|
|
|
|
|
|
|
|
/** 根据类目文字长度估算 Y 轴左侧留白(中文约 13px/字) */
|
|
|
|
|
const calcCategoryGridLeft = (labels: string[]) => {
|
|
|
|
|
const maxLen = labels.reduce((max, label) => Math.max(max, (label || '').length), 0);
|
|
|
|
|
return Math.min(maxLen * 0.1, 10);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const customerChartHeight = computed(() => {
|
|
|
|
|
const count = dashboardData.value.customerTransactionDistribution?.length || 0;
|
|
|
|
|
return Math.max(320, count * 38 + 72);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/** 加载仪表盘数据 */
|
|
|
|
|
const loadDashboard = async () => {
|
|
|
|
|
if (!checkPermi(DASHBOARD_QUERY_PERM)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!dateRange.value?.length || dateRange.value.length !== 2) {
|
|
|
|
|
proxy?.$modal.msgWarning('请选择签订时间区间');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res = await getSalesContractDashboard({
|
|
|
|
|
beginDate: dateRange.value[0],
|
|
|
|
|
endDate: dateRange.value[1]
|
|
|
|
|
});
|
|
|
|
|
dashboardData.value = res.data || createEmptyDashboard();
|
|
|
|
|
await nextTick();
|
|
|
|
|
renderCharts();
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDateChange = (value: [string, string] | null) => {
|
|
|
|
|
if (value?.length === 2) {
|
|
|
|
|
handleQuery();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleQuery = () => {
|
|
|
|
|
loadDashboard();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildRatioChartOption = (): echarts.EChartsOption => {
|
|
|
|
|
const data = dashboardData.value.businessDirectionRatios || [];
|
|
|
|
|
return {
|
|
|
|
|
color: CHART_COLORS.ratioPalette,
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'item',
|
|
|
|
|
formatter: (params: any) => {
|
|
|
|
|
const { name, value, percent } = params;
|
|
|
|
|
return `${name}<br/>${formatAmount(value)} (${percent}%)`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
orient: 'horizontal',
|
|
|
|
|
bottom: 0,
|
|
|
|
|
left: 'center',
|
|
|
|
|
itemWidth: 10,
|
|
|
|
|
itemHeight: 10,
|
|
|
|
|
textStyle: { fontSize: 12, color: '#606266' }
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: '业务方向销售额比例',
|
|
|
|
|
type: 'pie',
|
|
|
|
|
radius: ['42%', '68%'],
|
|
|
|
|
center: ['50%', '45%'],
|
|
|
|
|
avoidLabelOverlap: true,
|
|
|
|
|
label: { show: false },
|
|
|
|
|
emphasis: {
|
|
|
|
|
label: { show: true, fontSize: 14, fontWeight: 'bold' }
|
|
|
|
|
},
|
|
|
|
|
itemStyle: { borderRadius: 4 },
|
|
|
|
|
data: data.map((item) => ({ name: item.name, value: item.value ?? 0 }))
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildCustomerChartOption = (): echarts.EChartsOption => {
|
|
|
|
|
const list = dashboardData.value.customerTransactionDistribution || [];
|
|
|
|
|
const sorted = [...list].sort((a, b) => (a.amount ?? 0) - (b.amount ?? 0));
|
|
|
|
|
const customerNames = sorted.map((item) => item.customerName || '未知客户');
|
|
|
|
|
const gridLeft = calcCategoryGridLeft(customerNames);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
color: [CHART_COLORS.primary],
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
axisPointer: { type: 'shadow' },
|
|
|
|
|
formatter: (params: any) => {
|
|
|
|
|
const item = Array.isArray(params) ? params[0] : params;
|
|
|
|
|
return `${item.axisValue}<br/>成交额: ${formatAmountNumber(item.value)}`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
grid: { left: gridLeft, right: 56, top: 16, bottom: 16, containLabel: true },
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: 'value',
|
|
|
|
|
axisLabel: {
|
|
|
|
|
formatter: (value: number) => formatAmountNumber(value)
|
|
|
|
|
},
|
|
|
|
|
splitLine: { lineStyle: { type: 'dashed', color: '#E4E7ED' } }
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: 'category',
|
|
|
|
|
data: customerNames,
|
|
|
|
|
axisTick: { show: false },
|
|
|
|
|
axisLine: { lineStyle: { color: '#DCDFE6' } },
|
|
|
|
|
axisLabel: {
|
|
|
|
|
color: '#303133',
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
interval: 0,
|
|
|
|
|
hideOverlap: false,
|
|
|
|
|
align: 'right',
|
|
|
|
|
margin: 10
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: '成交额',
|
|
|
|
|
type: 'bar',
|
|
|
|
|
barMaxWidth: 28,
|
|
|
|
|
itemStyle: { borderRadius: [0, 6, 6, 0] },
|
|
|
|
|
label: {
|
|
|
|
|
show: true,
|
|
|
|
|
position: 'right',
|
|
|
|
|
color: '#606266',
|
|
|
|
|
formatter: (params: any) => (params.value > 0 ? formatAmountNumber(params.value) : '')
|
|
|
|
|
},
|
|
|
|
|
data: sorted.map((item) => item.amount ?? 0)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildRankingChartOption = (): echarts.EChartsOption => {
|
|
|
|
|
const list = dashboardData.value.salesContractRanking || [];
|
|
|
|
|
const sorted = [...list].sort((a, b) => (a.contractAmount ?? 0) - (b.contractAmount ?? 0));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
color: [CHART_COLORS.ranking],
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
axisPointer: { type: 'shadow' },
|
|
|
|
|
formatter: (params: any) => {
|
|
|
|
|
const item = Array.isArray(params) ? params[0] : params;
|
|
|
|
|
return `${item.axisValue}<br/>合同金额: ${formatAmountNumber(item.value)}`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
bottom: 8,
|
|
|
|
|
left: 'center',
|
|
|
|
|
data: ['合同金额'],
|
|
|
|
|
itemWidth: 12,
|
|
|
|
|
itemHeight: 12,
|
|
|
|
|
textStyle: { fontSize: 12, color: '#606266' }
|
|
|
|
|
},
|
|
|
|
|
grid: { left: 56, right: 24, top: 32, bottom: 48 },
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: 'category',
|
|
|
|
|
data: sorted.map((item) => item.salespersonName),
|
|
|
|
|
axisTick: { alignWithLabel: true },
|
|
|
|
|
axisLine: { lineStyle: { color: '#DCDFE6' } },
|
|
|
|
|
axisLabel: { color: '#303133' }
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: 'value',
|
|
|
|
|
axisLabel: {
|
|
|
|
|
formatter: (value: number) => formatAmountNumber(value)
|
|
|
|
|
},
|
|
|
|
|
splitLine: { lineStyle: { type: 'dashed', color: '#E4E7ED' } }
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: '合同金额',
|
|
|
|
|
type: 'bar',
|
|
|
|
|
barMaxWidth: 48,
|
|
|
|
|
itemStyle: { borderRadius: [6, 6, 0, 0] },
|
|
|
|
|
label: {
|
|
|
|
|
show: true,
|
|
|
|
|
position: 'top',
|
|
|
|
|
color: '#606266',
|
|
|
|
|
formatter: (params: any) => formatAmountNumber(params.value)
|
|
|
|
|
},
|
|
|
|
|
data: sorted.map((item) => item.contractAmount ?? 0)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const initChart = (el: HTMLElement | undefined, theme?: string) => {
|
|
|
|
|
if (!el) return null;
|
|
|
|
|
return echarts.getInstanceByDom(el) ?? echarts.init(el, theme);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderCharts = () => {
|
|
|
|
|
if (ratioChartRef.value) {
|
|
|
|
|
ratioChart = initChart(ratioChartRef.value);
|
|
|
|
|
ratioChart?.setOption(buildRatioChartOption(), true);
|
|
|
|
|
}
|
|
|
|
|
if (customerChartRef.value) {
|
|
|
|
|
customerChart = initChart(customerChartRef.value);
|
|
|
|
|
customerChart?.setOption(buildCustomerChartOption(), true);
|
|
|
|
|
}
|
|
|
|
|
if (rankingChartRef.value) {
|
|
|
|
|
rankingChart = initChart(rankingChartRef.value);
|
|
|
|
|
rankingChart?.setOption(buildRankingChartOption(), true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resizeCharts = () => {
|
|
|
|
|
ratioChart?.resize();
|
|
|
|
|
customerChart?.resize();
|
|
|
|
|
rankingChart?.resize();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const disposeCharts = () => {
|
|
|
|
|
ratioChart?.dispose();
|
|
|
|
|
customerChart?.dispose();
|
|
|
|
|
rankingChart?.dispose();
|
|
|
|
|
ratioChart = null;
|
|
|
|
|
customerChart = null;
|
|
|
|
|
rankingChart = null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRefresh = () => {
|
|
|
|
|
loadDashboard();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
watch(isFullscreen, () => {
|
|
|
|
|
nextTick(() => resizeCharts());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(customerChartHeight, () => {
|
|
|
|
|
nextTick(() => customerChart?.resize());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useResizeObserver(dashboardRef, () => {
|
|
|
|
|
resizeCharts();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadDashboard();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
disposeCharts();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.sales-contract-dashboard {
|
|
|
|
|
min-height: calc(100vh - 84px);
|
|
|
|
|
padding: 16px 20px 24px;
|
|
|
|
|
background: linear-gradient(180deg, #eef2f8 0%, #f5f7fa 120px, #f5f7fa 100%);
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
|
|
|
|
&.is-fullscreen {
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
padding: 20px 28px 28px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dashboard-header {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr auto 1fr;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
padding: 14px 20px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
box-shadow: 0 2px 12px rgba(31, 45, 61, 0.06);
|
|
|
|
|
|
|
|
|
|
&__filter {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__filter-label {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #606266;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__date-picker {
|
|
|
|
|
width: 280px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__title {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #1f2d3d;
|
|
|
|
|
text-align: center;
|
|
|
|
|
letter-spacing: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dashboard-body {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.metrics-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr 1.5fr;
|
|
|
|
|
gap: 18px;
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.metric-card {
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
min-height: 168px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
|
|
|
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 8px 20px rgba(31, 45, 61, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--summary {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__accent {
|
|
|
|
|
height: 4px;
|
|
|
|
|
background: linear-gradient(90deg, #e75e58 0%, #f08984 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 28px 20px 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__value-wrap {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__value {
|
|
|
|
|
font-size: 38px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
line-height: 1.1;
|
|
|
|
|
color: #e75e58;
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__unit {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #e75e58;
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__label {
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #606266;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--chart {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
padding: 16px 18px 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__chart-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
border-bottom: 1px solid #f0f2f5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__chart-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__chart-unit {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__chart {
|
|
|
|
|
flex: 1;
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 132px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.charts-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 18px;
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-panel {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
padding: 0 18px 16px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
|
|
|
|
|
transition: box-shadow 0.2s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
box-shadow: 0 8px 20px rgba(31, 45, 61, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 16px 0 12px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
border-bottom: 1px solid #f0f2f5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__title {
|
|
|
|
|
position: relative;
|
|
|
|
|
padding-left: 12px;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
|
|
|
|
&::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 50%;
|
|
|
|
|
width: 4px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
background: var(--el-color-primary);
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__unit {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
|
|
|
|
|
&--customer {
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|