| | |
| | | |
| | | 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' }); |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | 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', |
| | |
| | | loadData(); |
| | | }); |
| | | onBeforeUnmount(() => { |
| | | disposed = true; |
| | | requestSequence++; |
| | | if (dataRefreshTimer) clearInterval(dataRefreshTimer); |
| | | document.removeEventListener('fullscreenchange', onFullscreenChange); |
| | | resizeObserver?.disconnect(); |
| | |
| | | <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> |
| | |
| | | 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%); } |
| | | } |
| | | |
| | | /* ======== 空状态 ======== */ |