云
16 小时以前 c2e8526f8f857be5e3a8c6f2b39c5efd424de33e
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' });
@@ -24,26 +25,38 @@
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(background = false) {
  if (!meta.value) return;
  if (!meta.value || disposed || (background && fetching)) return;
  const sequence = ++requestSequence;
  const code = meta.value.code;
  fetching = true;
  if (!background && dataRefreshTimer) clearInterval(dataRefreshTimer);
  try {
    if (!background) loading.value = true;
    const data = await getDashboardData(meta.value.code);
    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 {
    if (!background) charts.value = getDefaultCharts();
    if (!background && !disposed && sequence === requestSequence) charts.value = getDefaultCharts();
  } finally {
    loading.value = false;
    await nextTick();
    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);
    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);
        }
      }
    }
  }
}
@@ -161,11 +174,6 @@
  return val;
}
// 大屏表格「实时滚动」:数据复制一份,配合 CSS marquee 做无缝纵向滚动
function duplicateRows(rows?: Record<string, unknown>[]): Record<string, unknown>[] {
  return rows ? [...rows, ...rows] : [];
}
// ======== 看板主题色 ========
const dashboardAccentColors: Record<string, string> = {
  'purchase-sales': '#6366f1',
@@ -205,6 +213,8 @@
  loadData();
});
onBeforeUnmount(() => {
  disposed = true;
  requestSequence++;
  if (dataRefreshTimer) clearInterval(dataRefreshTimer);
  document.removeEventListener('fullscreenchange', onFullscreenChange);
  resizeObserver?.disconnect();
@@ -311,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 class="marquee-tbody">
                <tr
                  v-for="(row, ri) in duplicateRows(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>
@@ -612,71 +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;
  overflow-y: hidden;
  max-height: 320px;
  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 {
  position: sticky;
  top: 0;
  z-index: 2;
}
.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); }
}
/* 大屏表格「实时滚动」:tbody 复制一份后 translateY(-50%) 无缝循环 */
.marquee-tbody {
  animation: table-marquee 15s linear infinite;
}
.marquee-tbody:hover {
  animation-play-state: paused;
}
@keyframes table-marquee {
  from { transform: translateY(0); }
  to { transform: translateY(-50%); }
}
/* ======== 空状态 ======== */