From dc1d067c566bbde1c7170186960a8bd27f210a47 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 09 九月 2026 13:45:58 +0800
Subject: [PATCH] feat(bi): 新增决策总览仪表盘并优化预测分析功能
---
src/views/bi/dashboard/index.vue | 270 +++++++++++++++++++++++++++++------------------------
1 files changed, 148 insertions(+), 122 deletions(-)
diff --git a/src/views/bi/dashboard/index.vue b/src/views/bi/dashboard/index.vue
index cac84a6..0685f54 100644
--- a/src/views/bi/dashboard/index.vue
+++ b/src/views/bi/dashboard/index.vue
@@ -40,21 +40,40 @@
}
}
-// ======== 鍒嗙被 ========
-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)));
+// ======== 12 鍒楁爡鏍艰嚜閫傚簲 ========
+const COMPACT_BREAKPOINT = 1180;
+const isCompact = ref(false);
+function updateCompact() {
+ isCompact.value = window.innerWidth < COMPACT_BREAKPOINT;
+}
-// ======== 鍥捐〃 Grid 鍒楄法搴� ========
-function isFullWidth(chart: BiDashboardApi.ChartItem): boolean {
- const w = chart.position?.w;
- if (w && w >= 24) return true;
+function isNumberCard(chart: BiDashboardApi.ChartItem): boolean {
+ return chart.chartType === 'number';
+}
+
+function isTableCard(chart: BiDashboardApi.ChartItem): boolean {
return chart.chartType === 'table';
}
-function getGridStyle(chart: BiDashboardApi.ChartItem): Record<string, string> {
- if (isFullWidth(chart)) return { 'grid-column': '1 / -1' };
- return {};
+function chartGridStyle(chart: BiDashboardApi.ChartItem): Record<string, string> {
+ const pos = chart.position;
+ if (isCompact.value) {
+ // 绐勫睆锛氭寚鏍囧崱鍗婂涓ゅ垪鎺掑竷锛屽浘琛�/琛ㄦ牸鍏ㄥ鍫嗗彔锛岄伩鍏嶆尋鍘�
+ return isNumberCard(chart) || chart.chartType === 'gauge'
+ ? { gridColumn: 'auto / span 6' }
+ : { gridColumn: '1 / -1' };
+ }
+ if (!pos) {
+ return { gridColumn: '1 / -1' };
+ }
+ const colStart = Math.max(1, (pos.x || 0) + 1);
+ const colSpan = Math.min(12, Math.max(1, pos.w || 12));
+ const rowStart = Math.max(1, (pos.y || 0) + 1);
+ const rowSpan = Math.max(1, pos.h || 1);
+ return {
+ gridColumn: `${colStart} / span ${colSpan}`,
+ gridRow: `${rowStart} / span ${rowSpan}`,
+ };
}
// ======== 鍥捐〃缂╂斁 ========
@@ -140,6 +159,9 @@
// ======== 鐪嬫澘涓婚鑹� ========
const dashboardAccentColors: Record<string, string> = {
+ 'decision-overview': '#00E5FF',
+ energy: '#FFC107',
+ 'sales-customer': '#00FF88',
'purchase-sales': '#6366f1',
'production-equipment': '#f59e0b',
quality: '#f43f5e',
@@ -160,6 +182,8 @@
if (name.includes('鍚堝悓') || name.includes('鍗忓悓')) return 'lucide:file-text';
if (name.includes('璁㈠崟')) return 'lucide:clipboard-list';
if (name.includes('搴撳瓨') || name.includes('浠撳簱')) return 'lucide:package';
+ if (name.includes('鑳借��') || name.includes('鐢甸噺') || name.includes('鍔熺巼') || name.includes('鐢ㄨ兘')) return 'lucide:zap';
+ if (name.includes('瀹㈡埛')) return 'lucide:users';
return 'lucide:bar-chart-4';
}
@@ -167,13 +191,17 @@
function getKpiColor(i: number) { return kpiColors[i % kpiColors.length]; }
watch(dashboardCode, () => loadData());
+watch(isCompact, () => setTimeout(resizeAllCharts, 120));
onMounted(() => {
document.addEventListener('fullscreenchange', onFullscreenChange);
+ window.addEventListener('resize', updateCompact);
+ updateCompact();
setupResizeObserver();
loadData();
});
onBeforeUnmount(() => {
document.removeEventListener('fullscreenchange', onFullscreenChange);
+ window.removeEventListener('resize', updateCompact);
resizeObserver?.disconnect();
});
</script>
@@ -227,87 +255,79 @@
<div style="height: 400px" />
</Spin>
- <!-- ======== 浠〃鐩樺唴瀹� ======== -->
+ <!-- ======== 12 鍒楁爡鏍煎竷灞� ======== -->
<template v-else>
- <!-- KPI 鍗$墖琛� -->
- <div v-if="numberCards.length > 0" class="kpi-row">
+ <div v-if="charts.length > 0" class="bi-grid">
<div
- v-for="(card, i) in numberCards"
- :key="card.id"
- class="kpi-card"
- :style="{ '--kpi-color': getKpiColor(i), animationDelay: `${i * 0.06}s` }"
+ v-for="(chart, i) in charts"
+ :key="chart.id"
+ class="bi-cell"
+ :style="chartGridStyle(chart)"
>
- <div class="kpi-icon">
- <IconifyIcon :icon="getKpiIcon(card.name)" />
+ <!-- 鏁板瓧鎸囨爣鍗� -->
+ <div
+ v-if="isNumberCard(chart)"
+ class="kpi-card"
+ :style="{ '--kpi-color': getKpiColor(i) }"
+ >
+ <div class="kpi-icon">
+ <IconifyIcon :icon="getKpiIcon(chart.name)" />
+ </div>
+ <div class="kpi-body">
+ <span class="kpi-label">{{ chart.name }}</span>
+ <span
+ class="kpi-value"
+ :ref="(el: unknown) => {
+ if (el && chart.data?.[0]) {
+ const v = extractNumberValue(chart.data[0] as Record<string, unknown>);
+ maybeAnimate(el as HTMLElement, chart.id!, v);
+ }
+ }"
+ >{{ getCardDisplayValue(chart) }}</span>
+ </div>
</div>
- <div class="kpi-body">
- <span class="kpi-label">{{ card.name }}</span>
- <span
- class="kpi-value"
- :ref="(el: unknown) => {
- if (el && card.data?.[0]) {
- const v = extractNumberValue(card.data[0] as Record<string, unknown>);
- maybeAnimate(el as HTMLElement, card.id!, v);
- }
- }"
- >{{ getCardDisplayValue(card) }}</span>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <div v-else-if="isTableCard(chart)" class="table-card">
+ <div class="table-header">
+ <span class="table-header-dot" :style="{ background: accentColor, boxShadow: `0 0 6px ${accentColor}` }" />
+ <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>
+ <Empty v-else description="鏆傛棤鏁版嵁" />
</div>
+
+ <!-- ECharts 鍥捐〃 -->
+ <ChartCard v-else :chart="chart" :accent-color="accentColor" />
</div>
</div>
-
- <!-- 鍥捐〃 Grid -->
- <div v-if="echartsCharts.length > 0" class="chart-row">
- <div
- v-for="(chart, i) in echartsCharts"
- :key="chart.id"
- :style="{ ...getGridStyle(chart), animationDelay: `${i * 0.08}s` }"
- >
- <ChartCard :chart="chart" :accent-color="accentColor" />
- </div>
- </div>
-
- <!-- 鏁版嵁琛ㄦ牸 -->
- <template v-if="tableCharts.length > 0">
- <div
- v-for="chart in tableCharts"
- :key="chart.id"
- class="table-card"
- >
- <div class="table-header">
- <span class="table-header-dot" :style="{ background: accentColor, boxShadow: `0 0 6px ${accentColor}` }" />
- <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>
- <Empty v-else description="鏆傛棤鏁版嵁" />
- </div>
- </template>
<!-- 绌虹姸鎬� -->
- <div v-if="charts.length === 0" class="empty-state">
+ <div v-else class="empty-state">
<IconifyIcon class="size-16 text-white/15" icon="lucide:bar-chart-4" />
<p>鏆傛湭閰嶇疆鍥捐〃鏁版嵁</p>
<p class="empty-sub">璇峰湪閰嶇疆绠$悊涓负褰撳墠浠〃鐩樻坊鍔犲浘琛�</p>
@@ -333,14 +353,17 @@
--text-muted: rgba(145, 158, 185, 0.5);
position: relative;
+ display: flex;
+ flex-direction: column;
background:
radial-gradient(ellipse 70% 50% at 50% 0%, #0d1f42 0%, #060e24 35%, #020817 100%);
color: var(--text-primary);
- padding: 16px 20px 24px;
+ padding: 16px 20px 20px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow-x: hidden;
overflow-y: auto;
- max-height: calc(100vh - 104px);
+ height: calc(100vh - 104px);
+ min-height: 320px;
}
/* 鑳屾櫙鍏夋檿 + 缃戞牸 */
@@ -375,6 +398,7 @@
.dash-header {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 16px;
+ flex-shrink: 0;
padding-bottom: 14px; margin-bottom: 16px;
border-bottom: 1px solid rgba(0,229,255,0.08);
}
@@ -451,16 +475,35 @@
}
.dash-fs-btn:hover { border-color: rgba(255,255,255,0.2); color: #fff; background: rgba(255,255,255,0.04); }
-/* ======== KPI 琛� ======== */
-.kpi-row {
+/* ======== 12 鍒楁爡鏍� ======== */
+.bi-grid {
position: relative; z-index: 1;
+ flex: 1;
+ min-height: 0;
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
- gap: 12px; margin-bottom: 16px;
+ grid-template-columns: repeat(12, minmax(0, 1fr));
+ grid-auto-rows: minmax(36px, 1fr);
+ gap: 12px;
}
+.bi-cell {
+ min-width: 0;
+ min-height: 0;
+ animation: kpi-fade-up 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
+}
+
+.bi-cell > * {
+ height: 100%;
+}
+
+/* ======== KPI 鎸囨爣鍗� ======== */
.kpi-card {
- display: flex; align-items: center; gap: 12px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ text-align: center;
padding: 14px 16px;
background: var(--bg-card);
backdrop-filter: blur(10px);
@@ -469,8 +512,6 @@
border-radius: 10px;
cursor: default;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
- animation: kpi-fade-up 0.55s cubic-bezier(0.4, 0, 0.2, 1) both;
- min-width: 0;
position: relative;
overflow: hidden;
}
@@ -491,8 +532,8 @@
.kpi-icon {
display: flex; align-items: center; justify-content: center;
- width: 40px; height: 40px; flex-shrink: 0;
- border-radius: 8px;
+ width: 42px; height: 42px; flex-shrink: 0;
+ border-radius: 10px;
background: color-mix(in srgb, var(--kpi-color, var(--cyan)) 15%, transparent);
color: var(--kpi-color, var(--cyan));
font-size: 18px;
@@ -500,17 +541,18 @@
}
.kpi-body {
- flex: 1; min-width: 0;
- display: flex; flex-direction: column; gap: 4px;
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
+ min-width: 0;
}
.kpi-label {
font-size: 11px; color: var(--text-secondary); letter-spacing: 0.3px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ max-width: 100%;
}
.kpi-value {
- font-size: 24px; font-weight: 800; line-height: 1;
+ font-size: 28px; font-weight: 800; line-height: 1.1;
font-variant-numeric: tabular-nums;
color: var(--text-primary);
}
@@ -520,32 +562,19 @@
to { opacity: 1; transform: translateY(0); }
}
-/* ======== 鍥捐〃琛� ======== */
-.chart-row {
- position: relative; z-index: 1;
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
- gap: 12px;
- margin-bottom: 16px;
-}
-
-.chart-row > div {
- min-width: 0;
- animation: kpi-fade-up 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
-}
-
/* ======== 琛ㄦ牸鍗$墖 ======== */
.table-card {
- position: relative; z-index: 1;
background: var(--bg-card);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px;
- margin-bottom: 16px;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
transition: all 0.35s;
- animation: kpi-fade-up 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
+ position: relative;
}
.table-card::before {
content: '';
@@ -582,7 +611,8 @@
}
.table-wrap {
- overflow-x: auto; border-radius: 8px;
+ flex: 1;
+ overflow-x: auto; overflow-y: auto; border-radius: 8px;
background: rgba(4, 14, 36, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
@@ -643,8 +673,7 @@
.is-fullscreen::after {
position: fixed;
}
-.is-fullscreen .kpi-row { gap: 16px; }
-.is-fullscreen .chart-row { gap: 16px; }
+.is-fullscreen .bi-grid { gap: 16px; }
.is-fullscreen .dash-header { margin-bottom: 24px; }
/* ======== 鍔犺浇鎬�/绌虹姸鎬� ======== */
@@ -657,9 +686,6 @@
.bi-dashboard { padding: 10px; }
.dash-header { flex-wrap: wrap; }
.dash-header-divider { display: none; }
- .kpi-row { grid-template-columns: 1fr 1fr; }
- .chart-row { grid-template-columns: 1fr; }
- .chart-row > div { grid-column: 1 / -1 !important; }
- .kpi-value { font-size: 20px; }
+ .kpi-value { font-size: 24px; }
}
-</style>
+</style>
\ No newline at end of file
--
Gitblit v1.9.3