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