From b35039bcd6b21e26bc01d2ad8de328456b272d16 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 26 十月 2026 10:56:41 +0800
Subject: [PATCH] Merge branch 'dev_海西州_北京同仁堂' into dev_海西州_惠牧

---
 src/views/bi/dashboard/index.vue |  136 ++++++++++++++++++---------------------------
 1 files changed, 54 insertions(+), 82 deletions(-)

diff --git a/src/views/bi/dashboard/index.vue b/src/views/bi/dashboard/index.vue
index c1bbf5c..87299c4 100644
--- a/src/views/bi/dashboard/index.vue
+++ b/src/views/bi/dashboard/index.vue
@@ -12,6 +12,7 @@
 
 import ChartCard from './modules/ChartCard.vue';
 import ClockWidget from './modules/ClockWidget.vue';
+import RealtimeTable from './modules/RealtimeTable.vue';
 import { DASHBOARD_META, getDashboardCode, getDefaultCharts } from './data';
 
 defineOptions({ name: 'BiDashboard' });
@@ -23,27 +24,53 @@
 const charts = ref<BiDashboardApi.ChartItem[]>([]);
 const loading = ref(true);
 const lastUpdateTime = ref('');
+let dataRefreshTimer: ReturnType<typeof setInterval> | undefined;
+let requestSequence = 0;
+let fetching = false;
+let disposed = false;
 
-async function loadData() {
-  if (!meta.value) return;
+async function loadData(background = false) {
+  if (!meta.value || disposed || (background && fetching)) return;
+  const sequence = ++requestSequence;
+  const code = meta.value.code;
+  fetching = true;
+  if (!background && dataRefreshTimer) clearInterval(dataRefreshTimer);
   try {
-    loading.value = true;
-    const data = await getDashboardData(meta.value.code);
+    if (!background) loading.value = true;
+    const data = await getDashboardData(code);
+    if (disposed || sequence !== requestSequence) return;
     charts.value = data.length > 0 ? data : getDefaultCharts();
     lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN', { hour12: false });
   } catch {
-    charts.value = getDefaultCharts();
+    if (!background && !disposed && sequence === requestSequence) charts.value = getDefaultCharts();
   } finally {
-    loading.value = false;
-    await nextTick();
-    resizeAllCharts();
+    if (!disposed && sequence === requestSequence) {
+      fetching = false;
+      loading.value = false;
+      await nextTick();
+      if (!disposed && sequence === requestSequence) {
+        resizeAllCharts();
+        if (!background) {
+          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 +129,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 +138,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 +185,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 +213,9 @@
   loadData();
 });
 onBeforeUnmount(() => {
+  disposed = true;
+  requestSequence++;
+  if (dataRefreshTimer) clearInterval(dataRefreshTimer);
   document.removeEventListener('fullscreenchange', onFullscreenChange);
   resizeObserver?.disconnect();
 });
@@ -249,7 +284,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) => {
@@ -286,30 +321,11 @@
             <span class="table-header-title">{{ chart.name }}</span>
             <span class="table-badge">瀹炴椂婊氬姩</span>
           </div>
-          <div v-if="chart.data && chart.data.length > 0" class="table-wrap">
-            <table class="data-table">
-              <thead>
-                <tr>
-                  <th
-                    v-for="key in Object.keys(chart.data[0] || {})"
-                    :key="key"
-                  >{{ key }}</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr
-                  v-for="(row, ri) in chart.data"
-                  :key="ri"
-                  :style="{ animationDelay: `${ri * 50}ms` }"
-                >
-                  <td
-                    v-for="key in Object.keys(chart.data[0] || {})"
-                    :key="key"
-                  >{{ getCellValue(row, key) }}</td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
+          <RealtimeTable
+            v-if="chart.data && chart.data.length > 0"
+            :rows="chart.data"
+            :format-cell="getCellValue"
+          />
           <Empty v-else description="鏆傛棤鏁版嵁" />
         </div>
       </template>
@@ -514,7 +530,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 {
@@ -587,50 +603,6 @@
   padding: 2px 8px; border-radius: 10px;
   background: rgba(0,255,136,0.08); color: var(--green);
   border: 1px solid rgba(0,255,136,0.15);
-}
-
-.table-wrap {
-  overflow-x: auto; border-radius: 8px;
-  background: rgba(4, 14, 36, 0.55);
-  backdrop-filter: blur(6px);
-  -webkit-backdrop-filter: blur(6px);
-  border: 1px solid rgba(0, 229, 255, 0.08);
-}
-
-.data-table {
-  width: 100%; border-collapse: collapse; font-size: 12px;
-}
-
-.data-table thead th {
-  background: rgba(6, 18, 42, 0.8);
-  color: var(--text-secondary);
-  font-weight: 600; font-size: 10px;
-  text-transform: uppercase; letter-spacing: 0.8px;
-  padding: 10px 14px; text-align: left;
-  border-bottom: 1px solid rgba(0, 229, 255, 0.12);
-  white-space: nowrap;
-}
-
-.data-table tbody td {
-  padding: 9px 14px; color: var(--text-primary);
-  border-bottom: 1px solid rgba(0, 229, 255, 0.04);
-  white-space: nowrap;
-}
-
-.data-table tbody tr {
-  transition: all 0.25s;
-  opacity: 0;
-  animation: row-fade-in 0.45s ease forwards;
-}
-
-.data-table tbody tr:hover {
-  background: rgba(0, 229, 255, 0.06);
-  box-shadow: inset 0 0 20px rgba(0, 229, 255, 0.03);
-}
-
-@keyframes row-fade-in {
-  from { opacity: 0; transform: translateY(6px); }
-  to { opacity: 1; transform: translateY(0); }
 }
 
 /* ======== 绌虹姸鎬� ======== */

--
Gitblit v1.9.3