From 973574fd62a1a130a1c229f812e70cfe3763b48e Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 14 十月 2026 09:54:10 +0800
Subject: [PATCH] feat(bi): 增加CRM数据展示功能并优化图表组件
---
src/views/bi/dashboard/index.vue | 38 ++++++++++++++----
src/views/bi/dashboard/modules/ChartCard.vue | 11 ++++-
src/views/bi/dashboard/data.ts | 2
src/views/bi/dashboard/chart-options.ts | 10 +++-
4 files changed, 46 insertions(+), 15 deletions(-)
diff --git a/src/views/bi/dashboard/chart-options.ts b/src/views/bi/dashboard/chart-options.ts
index f01bc66..ca6c55f 100644
--- a/src/views/bi/dashboard/chart-options.ts
+++ b/src/views/bi/dashboard/chart-options.ts
@@ -40,13 +40,17 @@
xData: string[],
series: Array<{ name: string; data: number[] }>,
colors: string[] = [C.cyan, C.blue, C.purple],
+ horizontal = false,
): EChartsOption {
return {
- grid: { bottom: 40, left: 48, right: 16, top: 16 },
+ grid: { bottom: 40, left: horizontal ? 140 : 48, right: 16, top: 16 },
legend: { ...darkLegend, bottom: 0 },
tooltip: { ...darkTooltip, axisPointer: { type: 'shadow' }, trigger: 'axis' },
- xAxis: { ...darkAxis, data: xData, type: 'category' },
- yAxis: { ...darkAxis, type: 'value' },
+ xAxis: horizontal ? { ...darkAxis, type: 'value' } : { ...darkAxis, data: xData, type: 'category' },
+ yAxis: horizontal ? {
+ ...darkAxis, data: xData, type: 'category', inverse: true,
+ axisLabel: { ...darkAxis.axisLabel, width: 125, overflow: 'truncate', interval: 0 },
+ } : { ...darkAxis, type: 'value' },
series: series.map((s, i) => ({
barGap: '30%',
barMaxWidth: 36,
diff --git a/src/views/bi/dashboard/data.ts b/src/views/bi/dashboard/data.ts
index 49dc1f9..c56c84c 100644
--- a/src/views/bi/dashboard/data.ts
+++ b/src/views/bi/dashboard/data.ts
@@ -13,7 +13,7 @@
'purchase-sales': {
code: 'purchase_sales',
title: '閲囪喘 路 閿�鍞� 路 鍞悗',
- subtitle: '渚涘簲閾惧叏閾捐矾缁忚惀鍐崇瓥鐪嬫澘',
+ subtitle: '瀹㈡埛缁忚惀 路 鍟嗘満璺熻繘 路 鍚堝悓鍥炴 路 閲囪喘鍞悗',
gradient: 'from-indigo-600 via-blue-600 to-cyan-500',
icon: 'lucide:shopping-cart',
},
diff --git a/src/views/bi/dashboard/index.vue b/src/views/bi/dashboard/index.vue
index c1bbf5c..70b9437 100644
--- a/src/views/bi/dashboard/index.vue
+++ b/src/views/bi/dashboard/index.vue
@@ -23,27 +23,41 @@
const charts = ref<BiDashboardApi.ChartItem[]>([]);
const loading = ref(true);
const lastUpdateTime = ref('');
+let dataRefreshTimer: ReturnType<typeof setInterval> | undefined;
-async function loadData() {
+async function loadData(background = false) {
if (!meta.value) return;
+ if (!background && dataRefreshTimer) clearInterval(dataRefreshTimer);
try {
- loading.value = true;
+ if (!background) loading.value = true;
const data = await getDashboardData(meta.value.code);
charts.value = data.length > 0 ? data : getDefaultCharts();
lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN', { hour12: false });
} catch {
- charts.value = getDefaultCharts();
+ if (!background) charts.value = getDefaultCharts();
} finally {
loading.value = false;
await nextTick();
resizeAllCharts();
+ if (!background && dashboardCode.value === 'purchase-sales') {
+ const intervals = charts.value.map((chart) => chart.refreshInterval).filter((interval) => interval > 0);
+ if (intervals.length) dataRefreshTimer = setInterval(() => {
+ if (!loading.value && document.visibilityState === 'visible') loadData(true);
+ }, Math.max(15, Math.min(...intervals)) * 1000);
+ }
}
}
// ======== 鍒嗙被 ========
-const numberCards = computed(() => charts.value.filter((c) => c.chartType === 'number'));
-const tableCharts = computed(() => charts.value.filter((c) => c.chartType === 'table'));
-const echartsCharts = computed(() => charts.value.filter((c) => !['number', 'table'].includes(c.chartType)));
+const displayCharts = computed(() => {
+ if (dashboardCode.value !== 'purchase-sales') return charts.value;
+ return [...charts.value].sort((a, b) =>
+ Number(b.chartOptions?.dashboardSection === 'crm') - Number(a.chartOptions?.dashboardSection === 'crm'),
+ );
+});
+const numberCards = computed(() => displayCharts.value.filter((c) => c.chartType === 'number'));
+const tableCharts = computed(() => displayCharts.value.filter((c) => c.chartType === 'table'));
+const echartsCharts = computed(() => displayCharts.value.filter((c) => !['number', 'table'].includes(c.chartType)));
// ======== 鍥捐〃 Grid 鍒楄法搴� ========
function isFullWidth(chart: BiDashboardApi.ChartItem): boolean {
@@ -102,6 +116,7 @@
const progress = Math.min((currentTime - startTime) / duration, 1);
const eased = 1 - (1 - progress) ** 3;
el.textContent = (end * eased).toFixed(decimals);
+ if (progress === 1) el.textContent = formatNumber(end);
if (progress < 1) requestAnimationFrame(update);
}
requestAnimationFrame(update);
@@ -110,7 +125,7 @@
function maybeAnimate(el: HTMLElement, cardId: number, value: number) {
if (animatedIds.has(cardId)) return;
animatedIds.add(cardId);
- animateValue(el, value, 0);
+ animateValue(el, value, Number.isInteger(value) ? 0 : 2);
}
watch(() => charts.value, () => {
@@ -157,6 +172,10 @@
// ======== KPI 鍥炬爣/棰滆壊 ========
function getKpiIcon(name: string): string {
+ if (name.includes('瀹㈡埛')) return 'lucide:users';
+ if (name.includes('鍟嗘満')) return 'lucide:target';
+ if (name.includes('鍥炴')) return 'lucide:wallet';
+ if (name.includes('鎶ヤ环')) return 'lucide:receipt-text';
if (name.includes('閲囪喘')) return 'lucide:shopping-cart';
if (name.includes('閿�鍞�')) return 'lucide:trending-up';
if (name.includes('鍞悗')) return 'lucide:headphones';
@@ -181,6 +200,7 @@
loadData();
});
onBeforeUnmount(() => {
+ if (dataRefreshTimer) clearInterval(dataRefreshTimer);
document.removeEventListener('fullscreenchange', onFullscreenChange);
resizeObserver?.disconnect();
});
@@ -249,7 +269,7 @@
<IconifyIcon :icon="getKpiIcon(card.name)" />
</div>
<div class="kpi-body">
- <span class="kpi-label">{{ card.name }}</span>
+ <span class="kpi-label" :title="card.name">{{ card.name }}</span>
<span
class="kpi-value"
:ref="(el: unknown) => {
@@ -514,7 +534,7 @@
.kpi-label {
font-size: 11px; color: var(--text-secondary); letter-spacing: 0.3px;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ white-space: normal; overflow-wrap: anywhere; line-height: 1.4;
}
.kpi-value {
diff --git a/src/views/bi/dashboard/modules/ChartCard.vue b/src/views/bi/dashboard/modules/ChartCard.vue
index 88e4152..93b23c0 100644
--- a/src/views/bi/dashboard/modules/ChartCard.vue
+++ b/src/views/bi/dashboard/modules/ChartCard.vue
@@ -41,12 +41,16 @@
const valueKey = keys[1] || 'value';
const labels = chart.data.map((d: Record<string, unknown>) => String(d[labelKey] || ''));
const values = chart.data.map((d: Record<string, unknown>) => Number(d[valueKey]) || 0);
+ const series = keys.slice(1).map((key) => ({
+ name: keys.length > 2 ? key : chart.name,
+ data: chart.data!.map((row) => Number(row[key]) || 0),
+ }));
switch (chart.chartType) {
case 'bar':
- return getBarChartOptions(labels, [{ name: chart.name, data: values }]);
+ return getBarChartOptions(labels, series, undefined, chart.chartOptions?.horizontal === true);
case 'line':
- return getAreaLineChartOptions(labels, [{ name: chart.name, data: values }]);
+ return getAreaLineChartOptions(labels, series);
case 'pie':
return getDonutPieChartOptions(labels.map((l, i) => ({ name: l, value: values[i] || 0 })));
case 'gauge': {
@@ -125,6 +129,9 @@
<div v-if="chart.error" class="flex h-[240px] items-center justify-center">
<Empty :description="chart.error" />
</div>
+ <div v-else-if="!chart.data?.length" class="flex h-[240px] items-center justify-center">
+ <Empty description="鏆傛棤涓氬姟鏁版嵁" />
+ </div>
<EchartsUI
v-else
ref="chartRef"
--
Gitblit v1.9.3