From c4a2d727fb1b72a1b7b7ae8c0d778f0e81952cce Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 03 八月 2026 11:28:41 +0800
Subject: [PATCH] feat(dashboard): 重构分析仪表盘页面并移除旧图表组件
---
src/views/dashboard/analytics/data.ts | 17
/dev/null | 55 --
src/views/dashboard/analytics/modules/quick-actions.vue | 73 +++
src/views/dashboard/analytics/modules/alert-panel.vue | 139 +++++++
src/views/dashboard/analytics/modules/kpi-cards.vue | 210 +++++++++++
src/views/dashboard/analytics/modules/production-trend.vue | 78 ++++
src/views/dashboard/analytics/index.vue | 197 ++++++----
src/views/dashboard/analytics/modules/biz-cards.vue | 218 +++++++++++
docs/dashboard-layout.canvas | 135 +++++++
9 files changed, 987 insertions(+), 135 deletions(-)
diff --git a/docs/dashboard-layout.canvas b/docs/dashboard-layout.canvas
new file mode 100644
index 0000000..3adba79
--- /dev/null
+++ b/docs/dashboard-layout.canvas
@@ -0,0 +1,135 @@
+{
+ "nodes": [
+ {
+ "id": "a1b2c3d4e5f60001",
+ "type": "text",
+ "x": 0,
+ "y": 0,
+ "width": 1200,
+ "height": 70,
+ "text": "# MOM 鍒堕�犺繍钀ュ钩鍙� 鈥� 棣栭〉浠〃鐩榎n**鎶�鏈爤**: Vue 3 + Ant Design Vue 4 + Tailwind CSS + ECharts | **椋庢牸**: Industrial Bento Grid + 鐜荤拑鎬佸崱鐗� | **閰嶈壊**: 宸ヤ笟钃� #2563eb"
+ },
+ {
+ "id": "a1b2c3d4e5f60002",
+ "type": "group",
+ "x": 0,
+ "y": 90,
+ "width": 1200,
+ "height": 100,
+ "label": "Row 0: 娆㈣繋鏍� 鈥� 鏃ユ湡 + 闂�欒 + 绯荤粺鐘舵��",
+ "color": "5"
+ },
+ {
+ "id": "a1b2c3d4e5f60003",
+ "type": "text",
+ "x": 20,
+ "y": 120,
+ "width": 1160,
+ "height": 50,
+ "text": "`<WelcomeBar />` 鈥� 宸︿晶: 鏃ユ湡 + \"鏃╀笂濂�/涓嬪崍濂�/鏅氫笂濂絓" 鏍规嵁鏃堕棿鍔ㄦ�佸垏鎹� | 鍙充晶: 绯荤粺杩愯鐘舵�佹寚绀哄櫒 + 鍒锋柊鎸夐挳"
+ },
+ {
+ "id": "a1b2c3d4e5f60004",
+ "type": "group",
+ "x": 0,
+ "y": 210,
+ "width": 1200,
+ "height": 150,
+ "label": "Row 1: 4 涓� KPI 鏍稿績鎸囨爣鍗$墖 (Bento Grid)",
+ "color": "4"
+ },
+ {
+ "id": "a1b2c3d4e5f60005",
+ "type": "text",
+ "x": 20,
+ "y": 240,
+ "width": 270,
+ "height": 100,
+ "text": "## 鐢熶骇璁㈠崟\n**杩涜涓�**: CountTo 鍔ㄧ敾\n寰呮帓浜� / 宸插畬鎴怽n\n馃帹 钃濊壊娓愬彉 `#2563eb 鈫� #60a5fa`\n馃敆 鐐瑰嚮璺宠浆 鈫� 鐢熶骇宸ュ崟鍒楄〃"
+ },
+ {
+ "id": "a1b2c3d4e5f60006",
+ "type": "text",
+ "x": 310,
+ "y": 240,
+ "width": 270,
+ "height": 100,
+ "text": "## 浠婃棩浜ч噺\n**浜ч噺**: CountTo 鍔ㄧ敾\n鏄ㄦ棩浜ч噺瀵规瘮\n\n馃帹 缁胯壊娓愬彉 `#16a34a 鈫� #4ade80`\n馃敆 鐐瑰嚮璺宠浆 鈫� 鐢熶骇鎶ュ伐"
+ },
+ {
+ "id": "a1b2c3d4e5f60007",
+ "type": "text",
+ "x": 600,
+ "y": 240,
+ "width": 270,
+ "height": 100,
+ "text": "## 璐ㄩ噺鍚堟牸鐜嘰n**鍚堟牸鐜�%**: CountTo 鍔ㄧ敾\n鍚堟牸鍝� / 涓嶈壇鍝乗n\n馃帹 姗欒壊娓愬彉 `#ea580c 鈫� #fb923c`\n馃敆 鐐瑰嚮璺宠浆 鈫� 璐ㄦ璁板綍"
+ },
+ {
+ "id": "a1b2c3d4e5f60008",
+ "type": "text",
+ "x": 890,
+ "y": 240,
+ "width": 270,
+ "height": 100,
+ "text": "## 璁惧杩愯鐜嘰n**杩愯涓� / 鎬绘暟**: CountTo 鍔ㄧ敾\n鍋滄満 / 缁存姢涓璡n\n馃帹 绱壊娓愬彉 `#7c3aed 鈫� #a78bfa`\n馃敆 鐐瑰嚮璺宠浆 鈫� 璁惧绠$悊"
+ },
+ {
+ "id": "a1b2c3d4e5f60009",
+ "type": "group",
+ "x": 0,
+ "y": 380,
+ "width": 1200,
+ "height": 300,
+ "label": "Row 2: 鍥捐〃鍖� 鈥� 鐢熶骇瓒嬪娍 + 棰勮闈㈡澘",
+ "color": "1"
+ },
+ {
+ "id": "a1b2c3d4e5f60010",
+ "type": "text",
+ "x": 20,
+ "y": 410,
+ "width": 700,
+ "height": 250,
+ "text": "## 鐢熶骇瓒嬪娍鍥� (`ProductionTrend`)\n**绫诲瀷**: ECharts 鎶樼嚎 + 鏌辩姸娣峰悎鍥綷n**鏁版嵁**: 杩� 7 澶╀骇閲� / 鍚堟牸鍝� / 涓嶈壇鍝乗n\n- X 杞�: 鏃ユ湡 (鏈�-鏃�)\n- Y 杞�: 鏁伴噺\n- 钃濊壊鏌�: 浜ч噺\n- 缁胯壊绾�: 鍚堟牸鍝乗n- 绾㈣壊绾�: 涓嶈壇鍝乗n\n`getProductionTrend(days=7)`\n\n馃搻 鍗犲: 14/24 鏍呮牸 (58%)"
+ },
+ {
+ "id": "a1b2c3d4e5f60011",
+ "type": "text",
+ "x": 740,
+ "y": 410,
+ "width": 440,
+ "height": 250,
+ "text": "## 棰勮涓庡緟鍔� (`AlertPanel`)\n\n### 瀹夌伅鍛煎彨\n鏈缃懠鍙暟 + 璺宠浆閾炬帴\n\n### 缁翠慨宸ュ崟\n寰呭鐞嗙淮淇崟鏁� + 璺宠浆閾炬帴\n\n### 宸ュ崟鐘舵�佸垎甯僜n鐜舰楗煎浘 `getWorkOrderStatusDistribution()`\n\n馃搻 鍗犲: 10/24 鏍呮牸 (42%)"
+ },
+ {
+ "id": "a1b2c3d4e5f60012",
+ "type": "group",
+ "x": 0,
+ "y": 700,
+ "width": 1200,
+ "height": 150,
+ "label": "Row 3: 蹇嵎鍏ュ彛 + 鍔ㄦ�佷俊鎭�",
+ "color": "6"
+ },
+ {
+ "id": "a1b2c3d4e5f60013",
+ "type": "text",
+ "x": 20,
+ "y": 730,
+ "width": 540,
+ "height": 100,
+ "text": "## 蹇嵎鎿嶄綔 (`QuickActions`)\n9 瀹牸蹇嵎鍏ュ彛:\n鐢熶骇宸ュ崟 | 鐢熶骇鎶ュ伐 | 璐ㄦ绠$悊\n璁惧鐐规 | 鐢熶骇鎺掔▼ | 瀹夌伅鍛煎彨\n鐗╂枡搴撳瓨 | 宸ヨ壓璺嚎 | 鏁版嵁鎶ヨ〃\n\n馃帹 鍥炬爣 + 鏍囩 鍗$墖鏍峰紡"
+ },
+ {
+ "id": "a1b2c3d4e5f60014",
+ "type": "text",
+ "x": 580,
+ "y": 730,
+ "width": 560,
+ "height": 100,
+ "text": "## 鏈�杩戝姩鎬� (`RecentActivity`)\n鏈�杩戞洿鏂扮殑宸ュ崟 / 鎶ュ伐璁板綍\n鏃堕棿绾� Timeline 缁勪欢\n\n绀轰緥:\n- 10:30 宸ュ崟 WO-2024-089 瀹屾垚鐢熶骇\n- 09:45 璐ㄦ閫氳繃 鎵规 B-0128\n- 08:00 璁惧 DEV-003 寮�濮嬬淮鎶�"
+ }
+ ],
+ "edges": []
+}
diff --git a/src/views/dashboard/analytics/analytics-trends.vue b/src/views/dashboard/analytics/analytics-trends.vue
deleted file mode 100644
index f1f0b23..0000000
--- a/src/views/dashboard/analytics/analytics-trends.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<script lang="ts" setup>
-import type { EchartsUIType } from '@vben/plugins/echarts';
-
-import { onMounted, ref } from 'vue';
-
-import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
-
-const chartRef = ref<EchartsUIType>();
-const { renderEcharts } = useEcharts(chartRef);
-
-onMounted(() => {
- renderEcharts({
- grid: {
- bottom: 0,
- containLabel: true,
- left: '1%',
- right: '1%',
- top: '2 %',
- },
- series: [
- {
- areaStyle: {},
- data: [
- 111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
- 36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
- 111,
- ],
- itemStyle: {
- color: '#5ab1ef',
- },
- smooth: true,
- type: 'line',
- },
- {
- areaStyle: {},
- data: [
- 33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
- 11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
- ],
- itemStyle: {
- color: '#019680',
- },
- smooth: true,
- type: 'line',
- },
- ],
- tooltip: {
- axisPointer: {
- lineStyle: {
- color: '#019680',
- width: 1,
- },
- },
- trigger: 'axis',
- },
- // xAxis: {
- // axisTick: {
- // show: false,
- // },
- // boundaryGap: false,
- // data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
- // type: 'category',
- // },
- xAxis: {
- axisTick: {
- show: false,
- },
- boundaryGap: false,
- data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
- splitLine: {
- lineStyle: {
- type: 'solid',
- width: 1,
- },
- show: true,
- },
- type: 'category',
- },
- yAxis: [
- {
- axisTick: {
- show: false,
- },
- max: 80_000,
- splitArea: {
- show: true,
- },
- splitNumber: 4,
- type: 'value',
- },
- ],
- });
-});
-</script>
-
-<template>
- <EchartsUI ref="chartRef" />
-</template>
diff --git a/src/views/dashboard/analytics/analytics-visits-data.vue b/src/views/dashboard/analytics/analytics-visits-data.vue
deleted file mode 100644
index 190fb41..0000000
--- a/src/views/dashboard/analytics/analytics-visits-data.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<script lang="ts" setup>
-import type { EchartsUIType } from '@vben/plugins/echarts';
-
-import { onMounted, ref } from 'vue';
-
-import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
-
-const chartRef = ref<EchartsUIType>();
-const { renderEcharts } = useEcharts(chartRef);
-
-onMounted(() => {
- renderEcharts({
- legend: {
- bottom: 0,
- data: ['璁块棶', '瓒嬪娍'],
- },
- radar: {
- indicator: [
- {
- name: '缃戦〉',
- },
- {
- name: '绉诲姩绔�',
- },
- {
- name: 'Ipad',
- },
- {
- name: '瀹㈡埛绔�',
- },
- {
- name: '绗笁鏂�',
- },
- {
- name: '鍏跺畠',
- },
- ],
- radius: '60%',
- splitNumber: 8,
- },
- series: [
- {
- areaStyle: {
- opacity: 1,
- shadowBlur: 0,
- shadowColor: 'rgba(0,0,0,.2)',
- shadowOffsetX: 0,
- shadowOffsetY: 10,
- },
- data: [
- {
- itemStyle: {
- color: '#b6a2de',
- },
- name: '璁块棶',
- value: [90, 50, 86, 40, 50, 20],
- },
- {
- itemStyle: {
- color: '#5ab1ef',
- },
- name: '瓒嬪娍',
- value: [70, 75, 70, 76, 20, 85],
- },
- ],
- itemStyle: {
- // borderColor: '#fff',
- borderRadius: 10,
- borderWidth: 2,
- },
- symbolSize: 0,
- type: 'radar',
- },
- ],
- tooltip: {},
- });
-});
-</script>
-
-<template>
- <EchartsUI ref="chartRef" />
-</template>
diff --git a/src/views/dashboard/analytics/analytics-visits-sales.vue b/src/views/dashboard/analytics/analytics-visits-sales.vue
deleted file mode 100644
index 6ff5208..0000000
--- a/src/views/dashboard/analytics/analytics-visits-sales.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<script lang="ts" setup>
-import type { EchartsUIType } from '@vben/plugins/echarts';
-
-import { onMounted, ref } from 'vue';
-
-import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
-
-const chartRef = ref<EchartsUIType>();
-const { renderEcharts } = useEcharts(chartRef);
-
-onMounted(() => {
- renderEcharts({
- series: [
- {
- animationDelay() {
- return Math.random() * 400;
- },
- animationEasing: 'exponentialInOut',
- animationType: 'scale',
- center: ['50%', '50%'],
- color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
- data: [
- { name: '澶栧寘', value: 500 },
- { name: '瀹氬埗', value: 310 },
- { name: '鎶�鏈敮鎸�', value: 274 },
- { name: '杩滅▼', value: 400 },
- ].toSorted((a, b) => {
- return a.value - b.value;
- }),
- name: '鍟嗕笟鍗犳瘮',
- radius: '80%',
- roseType: 'radius',
- type: 'pie',
- },
- ],
-
- tooltip: {
- trigger: 'item',
- },
- });
-});
-</script>
-
-<template>
- <EchartsUI ref="chartRef" />
-</template>
diff --git a/src/views/dashboard/analytics/analytics-visits-source.vue b/src/views/dashboard/analytics/analytics-visits-source.vue
deleted file mode 100644
index 0915c7a..0000000
--- a/src/views/dashboard/analytics/analytics-visits-source.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script lang="ts" setup>
-import type { EchartsUIType } from '@vben/plugins/echarts';
-
-import { onMounted, ref } from 'vue';
-
-import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
-
-const chartRef = ref<EchartsUIType>();
-const { renderEcharts } = useEcharts(chartRef);
-
-onMounted(() => {
- renderEcharts({
- legend: {
- bottom: '2%',
- left: 'center',
- },
- series: [
- {
- animationDelay() {
- return Math.random() * 100;
- },
- animationEasing: 'exponentialInOut',
- animationType: 'scale',
- avoidLabelOverlap: false,
- color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
- data: [
- { name: '鎼滅储寮曟搸', value: 1048 },
- { name: '鐩存帴璁块棶', value: 735 },
- { name: '閭欢钀ラ攢', value: 580 },
- { name: '鑱旂洘骞垮憡', value: 484 },
- ],
- emphasis: {
- label: {
- fontSize: '12',
- fontWeight: 'bold',
- show: true,
- },
- },
- itemStyle: {
- // borderColor: '#fff',
- borderRadius: 10,
- borderWidth: 2,
- },
- label: {
- position: 'center',
- show: false,
- },
- labelLine: {
- show: false,
- },
- name: '璁块棶鏉ユ簮',
- radius: ['40%', '65%'],
- type: 'pie',
- },
- ],
- tooltip: {
- trigger: 'item',
- },
- });
-});
-</script>
-
-<template>
- <EchartsUI ref="chartRef" />
-</template>
diff --git a/src/views/dashboard/analytics/analytics-visits.vue b/src/views/dashboard/analytics/analytics-visits.vue
deleted file mode 100644
index 7e0f101..0000000
--- a/src/views/dashboard/analytics/analytics-visits.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<script lang="ts" setup>
-import type { EchartsUIType } from '@vben/plugins/echarts';
-
-import { onMounted, ref } from 'vue';
-
-import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
-
-const chartRef = ref<EchartsUIType>();
-const { renderEcharts } = useEcharts(chartRef);
-
-onMounted(() => {
- renderEcharts({
- grid: {
- bottom: 0,
- containLabel: true,
- left: '1%',
- right: '1%',
- top: '2 %',
- },
- series: [
- {
- barMaxWidth: 80,
- // color: '#4f69fd',
- data: [
- 3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
- 3200, 4800,
- ],
- type: 'bar',
- },
- ],
- tooltip: {
- axisPointer: {
- lineStyle: {
- // color: '#4f69fd',
- width: 1,
- },
- },
- trigger: 'axis',
- },
- xAxis: {
- data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}鏈坄),
- type: 'category',
- },
- yAxis: {
- max: 8000,
- splitNumber: 4,
- type: 'value',
- },
- });
-});
-</script>
-
-<template>
- <EchartsUI ref="chartRef" />
-</template>
diff --git a/src/views/dashboard/analytics/data.ts b/src/views/dashboard/analytics/data.ts
new file mode 100644
index 0000000..3de09c7
--- /dev/null
+++ b/src/views/dashboard/analytics/data.ts
@@ -0,0 +1,17 @@
+import type { MesHomeApi } from '#/api/mes/home';
+
+export const defaultSummary: MesHomeApi.Summary = {
+ workOrderActiveCount: 0,
+ workOrderPrepareCount: 0,
+ workOrderFinishedCount: 0,
+ todayOutput: 0,
+ yesterdayOutput: 0,
+ todayQualifiedQuantity: 0,
+ todayUnqualifiedQuantity: 0,
+ machineryTotal: 0,
+ machineryProducing: 0,
+ machineryStop: 0,
+ machineryMaintenance: 0,
+ andonActiveCount: 0,
+ repairActiveCount: 0,
+};
diff --git a/src/views/dashboard/analytics/index.vue b/src/views/dashboard/analytics/index.vue
index e794c99..6085aef 100644
--- a/src/views/dashboard/analytics/index.vue
+++ b/src/views/dashboard/analytics/index.vue
@@ -1,90 +1,127 @@
<script lang="ts" setup>
-import type { AnalysisOverviewItem } from '@vben/common-ui';
-import type { TabOption } from '@vben/types';
+import { computed, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
-import {
- AnalysisChartCard,
- AnalysisChartsTabs,
- AnalysisOverview,
-} from '@vben/common-ui';
-import {
- SvgBellIcon,
- SvgCakeIcon,
- SvgCardIcon,
- SvgDownloadIcon,
-} from '@vben/icons';
+import { Page } from '@vben/common-ui';
-import AnalyticsTrends from './analytics-trends.vue';
-import AnalyticsVisitsData from './analytics-visits-data.vue';
-import AnalyticsVisitsSales from './analytics-visits-sales.vue';
-import AnalyticsVisitsSource from './analytics-visits-source.vue';
-import AnalyticsVisits from './analytics-visits.vue';
+import { Card, Col, Row } from 'ant-design-vue';
-const overviewItems: AnalysisOverviewItem[] = [
- {
- icon: SvgCardIcon,
- title: '鐢ㄦ埛閲�',
- totalTitle: '鎬荤敤鎴烽噺',
- totalValue: 120_000,
- value: 2000,
- },
- {
- icon: SvgCakeIcon,
- title: '璁块棶閲�',
- totalTitle: '鎬昏闂噺',
- totalValue: 500_000,
- value: 20_000,
- },
- {
- icon: SvgDownloadIcon,
- title: '涓嬭浇閲�',
- totalTitle: '鎬讳笅杞介噺',
- totalValue: 120_000,
- value: 8000,
- },
- {
- icon: SvgBellIcon,
- title: '浣跨敤閲�',
- totalTitle: '鎬讳娇鐢ㄩ噺',
- totalValue: 50_000,
- value: 5000,
- },
-];
+import { getHomeSummary } from '#/api/mes/home';
-const chartTabs: TabOption[] = [
- {
- label: '娴侀噺瓒嬪娍',
- value: 'trends',
- },
- {
- label: '鏈堣闂噺',
- value: 'visits',
- },
-];
+import { defaultSummary } from './data';
+import AlertPanel from './modules/alert-panel.vue';
+import BizCards from './modules/biz-cards.vue';
+import KpiCards from './modules/kpi-cards.vue';
+import ProductionTrend from './modules/production-trend.vue';
+import QuickActions from './modules/quick-actions.vue';
+
+defineOptions({ name: 'DashboardAnalytics' });
+
+const router = useRouter();
+const summary = ref(defaultSummary);
+
+const greeting = computed(() => {
+ const hour = new Date().getHours();
+ if (hour < 6) return '澶滄繁浜�';
+ if (hour < 9) return '鏃╀笂濂�';
+ if (hour < 12) return '涓婂崍濂�';
+ if (hour < 14) return '涓崍濂�';
+ if (hour < 18) return '涓嬪崍濂�';
+ return '鏅氫笂濂�';
+});
+
+const today = computed(() => {
+ const d = new Date();
+ const weekNames = ['鏃�', '涓�', '浜�', '涓�', '鍥�', '浜�', '鍏�'];
+ return `${d.getFullYear()}骞�${d.getMonth() + 1}鏈�${d.getDate()}鏃� 鏄熸湡${weekNames[d.getDay()]}`;
+});
+
+function handleNavigate(name: string) {
+ router.push({ name });
+}
+
+async function loadSummary() {
+ summary.value = await getHomeSummary();
+}
+
+onMounted(() => {
+ loadSummary();
+});
</script>
<template>
- <div class="p-5">
- <AnalysisOverview :items="overviewItems" />
- <AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
- <template #trends>
- <AnalyticsTrends />
- </template>
- <template #visits>
- <AnalyticsVisits />
- </template>
- </AnalysisChartsTabs>
-
- <div class="mt-5 w-full md:flex">
- <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="璁块棶鏁伴噺">
- <AnalyticsVisitsData />
- </AnalysisChartCard>
- <AnalysisChartCard class="mt-5 md:mt-0 md:mr-4 md:w-1/3" title="璁块棶鏉ユ簮">
- <AnalyticsVisitsSource />
- </AnalysisChartCard>
- <AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="璁块棶鏉ユ簮">
- <AnalyticsVisitsSales />
- </AnalysisChartCard>
+ <Page auto-content-height>
+ <!-- 娆㈣繋鏍� -->
+ <div class="glass-card mb-6 flex items-center justify-between rounded-2xl p-6">
+ <div>
+ <h1 class="bg-gradient-to-r from-blue-600 via-purple-600 to-pink-500 bg-clip-text text-2xl font-bold text-transparent dark:from-blue-400 dark:via-purple-400 dark:to-pink-400">
+ {{ greeting }}锛屾杩庡洖鏉�
+ </h1>
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ today }}</p>
+ </div>
+ <div class="hidden items-center gap-2 sm:flex">
+ <div class="flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
+ <span class="relative flex size-2">
+ <span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75" />
+ <span class="relative inline-flex size-2 rounded-full bg-emerald-500" />
+ </span>
+ 绯荤粺杩愯涓�
+ </div>
+ </div>
</div>
- </div>
+
+ <!-- MES 鐢熶骇 KPI -->
+ <KpiCards :summary="summary" @navigate="handleNavigate" />
+
+ <!-- ERP/WMS/BPM 缁忚惀 KPI -->
+ <BizCards @navigate="handleNavigate" />
+
+ <!-- 鐢熶骇瓒嬪娍 + 棰勮闈㈡澘 -->
+ <Row :gutter="16" class="mb-4">
+ <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4">
+ <div class="glass-card rounded-2xl p-5">
+ <h3 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-100">
+ 鐢熶骇瓒嬪娍锛堣繎7澶╋級
+ </h3>
+ <ProductionTrend />
+ </div>
+ </Col>
+ <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4">
+ <AlertPanel
+ :andon-active-count="summary.andonActiveCount"
+ :repair-active-count="summary.repairActiveCount"
+ @navigate="handleNavigate"
+ />
+ </Col>
+ </Row>
+
+ <!-- 蹇嵎鎿嶄綔 -->
+ <Row :gutter="16" class="mb-4">
+ <Col :span="24">
+ <QuickActions @navigate="handleNavigate" />
+ </Col>
+ </Row>
+ </Page>
</template>
+
+<style scoped>
+.glass-card {
+ background: linear-gradient(135deg, rgba(255,255,255,0.8), rgba(255,255,255,0.5));
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255,255,255,0.6);
+ box-shadow:
+ 0 8px 32px rgba(0,0,0,0.04),
+ 0 2px 8px rgba(0,0,0,0.02),
+ inset 0 1px 0 rgba(255,255,255,0.8);
+}
+
+.dark .glass-card {
+ background: linear-gradient(135deg, rgba(30,30,50,0.7), rgba(20,20,40,0.5));
+ border: 1px solid rgba(255,255,255,0.08);
+ box-shadow:
+ 0 8px 32px rgba(0,0,0,0.3),
+ 0 2px 8px rgba(0,0,0,0.2),
+ inset 0 1px 0 rgba(255,255,255,0.05);
+}
+</style>
diff --git a/src/views/dashboard/analytics/modules/alert-panel.vue b/src/views/dashboard/analytics/modules/alert-panel.vue
new file mode 100644
index 0000000..4be1544
--- /dev/null
+++ b/src/views/dashboard/analytics/modules/alert-panel.vue
@@ -0,0 +1,139 @@
+<script lang="ts" setup>
+import type { MesHomeApi } from '#/api/mes/home';
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
+import { onMounted, ref } from 'vue';
+
+import { CountTo } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Spin } from 'ant-design-vue';
+
+import { getWorkOrderStatusDistribution } from '#/api/mes/home';
+
+defineOptions({ name: 'DashboardAlertPanel' });
+
+const props = defineProps<{
+ andonActiveCount: number;
+ repairActiveCount: number;
+}>();
+
+const emit = defineEmits<{
+ navigate: [name: string];
+}>();
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+const loading = ref(false);
+
+async function loadStatus() {
+ loading.value = true;
+ try {
+ const data = await getWorkOrderStatusDistribution();
+ if (data.length > 0) {
+ renderEcharts({
+ legend: { bottom: 0, textStyle: { fontSize: 11 } },
+ series: [
+ {
+ animationDuration: 1200,
+ animationEasing: 'cubicOut',
+ color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#ec4899'],
+ data: data.map((d: MesHomeApi.WorkOrderStatus) => ({ name: d.statusName, value: d.count })),
+ emphasis: {
+ itemStyle: { shadowBlur: 20, shadowColor: 'rgba(0,0,0,0.2)', shadowOffsetX: 0 },
+ scaleSize: 8,
+ },
+ itemStyle: { borderRadius: 6, borderColor: 'transparent', borderWidth: 3 },
+ label: { show: false },
+ name: '宸ュ崟鐘舵��',
+ radius: ['50%', '75%'],
+ type: 'pie',
+ },
+ ],
+ tooltip: { trigger: 'item' },
+ });
+ }
+ } finally {
+ loading.value = false;
+ }
+}
+
+onMounted(() => {
+ loadStatus();
+});
+</script>
+
+<template>
+ <div class="space-y-4">
+ <!-- 棰勮鍗$墖琛� -->
+ <div class="grid grid-cols-2 gap-3">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4"
+ @click="emit('navigate', 'MesProAndon')"
+ >
+ <div class="relative z-10 flex items-center gap-3">
+ <div class="flex size-10 items-center justify-center rounded-lg bg-red-50 text-red-600 shadow-sm dark:bg-red-500/15 dark:text-red-400">
+ <IconifyIcon class="size-5" icon="lucide:alert-triangle" />
+ </div>
+ <div>
+ <div class="text-xs text-gray-500 dark:text-gray-400">瀹夌伅鍛煎彨</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-xl font-bold text-red-600 dark:text-red-400" :end-val="andonActiveCount" :duration="1000" />
+ <span class="text-xs text-gray-400">鏈缃�</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4"
+ @click="emit('navigate', 'MesProRepair')"
+ >
+ <div class="relative z-10 flex items-center gap-3">
+ <div class="flex size-10 items-center justify-center rounded-lg bg-amber-50 text-amber-600 shadow-sm dark:bg-amber-500/15 dark:text-amber-400">
+ <IconifyIcon class="size-5" icon="lucide:wrench" />
+ </div>
+ <div>
+ <div class="text-xs text-gray-500 dark:text-gray-400">缁翠慨宸ュ崟</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-xl font-bold text-amber-600 dark:text-amber-400" :end-val="repairActiveCount" :duration="1000" />
+ <span class="text-xs text-gray-400">寰呭鐞�</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 宸ュ崟鐘舵�佸浘 -->
+ <div class="glass-tilt rounded-2xl p-5">
+ <h4 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-100">
+ 宸ュ崟鐘舵�佸垎甯�
+ </h4>
+ <Spin :spinning="loading">
+ <EchartsUI ref="chartRef" class="!h-[200px]" />
+ </Spin>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.glass-tilt {
+ background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55));
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255,255,255,0.6);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.04),
+ 0 1px 4px rgba(0,0,0,0.02),
+ inset 0 1px 0 rgba(255,255,255,0.8);
+}
+
+.dark .glass-tilt {
+ background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5));
+ border: 1px solid rgba(255,255,255,0.08);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.3),
+ 0 1px 4px rgba(0,0,0,0.2),
+ inset 0 1px 0 rgba(255,255,255,0.04);
+}
+</style>
diff --git a/src/views/dashboard/analytics/modules/biz-cards.vue b/src/views/dashboard/analytics/modules/biz-cards.vue
new file mode 100644
index 0000000..95a9e63
--- /dev/null
+++ b/src/views/dashboard/analytics/modules/biz-cards.vue
@@ -0,0 +1,218 @@
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
+
+import { CountTo } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Col, Row } from 'ant-design-vue';
+
+import { getPurchaseOrderPage } from '#/api/erp/purchase/order';
+import { getSaleOrderPage } from '#/api/erp/sale/order';
+import { getMaterialStockPage } from '#/api/mes/wm/materialstock';
+import { getTaskTodoPage } from '#/api/bpm/task';
+
+defineOptions({ name: 'DashboardBizCards' });
+
+const emit = defineEmits<{
+ navigate: [name: string];
+}>();
+
+const purchaseCount = ref(0);
+const saleCount = ref(0);
+const stockCount = ref(0);
+const bpmTodoCount = ref(0);
+
+async function loadPurchase() {
+ try {
+ const result = await getPurchaseOrderPage({ pageNo: 1, pageSize: 1 });
+ purchaseCount.value = result.total ?? 0;
+ } catch { /* 鏃犳潈闄愭垨鎺ュ彛寮傚父 */ }
+}
+
+async function loadSale() {
+ try {
+ const result = await getSaleOrderPage({ pageNo: 1, pageSize: 1 });
+ saleCount.value = result.total ?? 0;
+ } catch { /* 鏃犳潈闄愭垨鎺ュ彛寮傚父 */ }
+}
+
+async function loadStock() {
+ try {
+ const result = await getMaterialStockPage({ pageNo: 1, pageSize: 1 });
+ stockCount.value = result.total ?? 0;
+ } catch { /* 鏃犳潈闄愭垨鎺ュ彛寮傚父 */ }
+}
+
+async function loadBpmTodo() {
+ try {
+ const result = await getTaskTodoPage({ pageNo: 1, pageSize: 1 });
+ bpmTodoCount.value = result.total ?? 0;
+ } catch { /* 鏃犳潈闄愭垨鎺ュ彛寮傚父 */ }
+}
+
+/** 3D 鍊炬枩 */
+interface TiltState { x: number; y: number }
+const tilts = ref<Record<string, TiltState>>({});
+
+function onMouseMove(e: MouseEvent, id: string) {
+ const el = e.currentTarget as HTMLElement;
+ const rect = el.getBoundingClientRect();
+ const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
+ const y = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
+ tilts.value[id] = { x: x * -5, y: y * 5 };
+}
+
+function onMouseLeave(id: string) {
+ tilts.value[id] = { x: 0, y: 0 };
+}
+
+function tiltStyle(id: string) {
+ const t = tilts.value[id] ?? { x: 0, y: 0 };
+ return {
+ transform: `perspective(500px) rotateX(${t.y}deg) rotateY(${t.x}deg)`,
+ transition: t.x === 0 && t.y === 0 ? 'transform 0.5s ease' : 'transform 0.1s ease',
+ };
+}
+
+onMounted(() => {
+ loadPurchase();
+ loadSale();
+ loadStock();
+ loadBpmTodo();
+});
+</script>
+
+<template>
+ <Row :gutter="16">
+ <!-- 閲囪喘璁㈠崟 -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('purchase')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'purchase')"
+ @mouseleave="onMouseLeave('purchase')"
+ @click="emit('navigate', 'ErpPurchaseOrder')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500 to-teal-400 shadow-lg shadow-cyan-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:shopping-cart" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">閲囪喘璁㈠崟</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-2xl font-bold leading-tight text-cyan-600 dark:text-cyan-400" :end-val="purchaseCount" :duration="1200" />
+ <span class="text-xs text-gray-400">绗�</span>
+ </div>
+ <div class="mt-1 text-xs text-gray-400">ERP 閲囪喘绠$悊</div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-cyan-500/5 to-teal-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-cyan-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+
+ <!-- 閿�鍞鍗� -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('sale')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'sale')"
+ @mouseleave="onMouseLeave('sale')"
+ @click="emit('navigate', 'ErpSaleOrder')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-pink-500 to-rose-400 shadow-lg shadow-pink-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:trending-up" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">閿�鍞鍗�</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-2xl font-bold leading-tight text-pink-600 dark:text-pink-400" :end-val="saleCount" :duration="1200" />
+ <span class="text-xs text-gray-400">绗�</span>
+ </div>
+ <div class="mt-1 text-xs text-gray-400">ERP 閿�鍞鐞�</div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-pink-500/5 to-rose-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-pink-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+
+ <!-- 鐗╂枡搴撳瓨 -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('stock')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'stock')"
+ @mouseleave="onMouseLeave('stock')"
+ @click="emit('navigate', 'MesWmMaterialStock')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-amber-500 to-yellow-400 shadow-lg shadow-amber-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:package-search" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">鐗╂枡搴撳瓨</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-2xl font-bold leading-tight text-amber-600 dark:text-amber-400" :end-val="stockCount" :duration="1200" />
+ <span class="text-xs text-gray-400">鏉�</span>
+ </div>
+ <div class="mt-1 text-xs text-gray-400">MES 搴撳瓨鍙拌处</div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-amber-500/5 to-yellow-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-amber-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+
+ <!-- BPM 寰呭姙 -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('bpm')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'bpm')"
+ @mouseleave="onMouseLeave('bpm')"
+ @click="emit('navigate', 'BpmTaskTodo')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-red-500 to-rose-400 shadow-lg shadow-red-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:clipboard-check" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">鎴戠殑寰呭姙</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-2xl font-bold leading-tight text-red-600 dark:text-red-400" :end-val="bpmTodoCount" :duration="1200" />
+ <span class="text-xs text-gray-400">椤�</span>
+ </div>
+ <div class="mt-1 text-xs text-gray-400">BPM 寰呭鎵�</div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-red-500/5 to-rose-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-red-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+ </Row>
+</template>
+
+<style scoped>
+.glass-tilt {
+ background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55));
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255,255,255,0.6);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.04),
+ 0 1px 4px rgba(0,0,0,0.02),
+ inset 0 1px 0 rgba(255,255,255,0.8);
+ transform-style: preserve-3d;
+}
+
+.dark .glass-tilt {
+ background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5));
+ border: 1px solid rgba(255,255,255,0.08);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.3),
+ 0 1px 4px rgba(0,0,0,0.2),
+ inset 0 1px 0 rgba(255,255,255,0.04);
+}
+</style>
diff --git a/src/views/dashboard/analytics/modules/kpi-cards.vue b/src/views/dashboard/analytics/modules/kpi-cards.vue
new file mode 100644
index 0000000..761db56
--- /dev/null
+++ b/src/views/dashboard/analytics/modules/kpi-cards.vue
@@ -0,0 +1,210 @@
+<script lang="ts" setup>
+import type { MesHomeApi } from '#/api/mes/home';
+
+import { computed, ref } from 'vue';
+
+import { CountTo } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Col, Row } from 'ant-design-vue';
+
+defineOptions({ name: 'DashboardKpiCards' });
+
+const props = defineProps<{
+ summary: MesHomeApi.Summary;
+}>();
+
+const emit = defineEmits<{
+ navigate: [name: string];
+}>();
+
+const qualityRate = computed(() => {
+ const total =
+ props.summary.todayQualifiedQuantity +
+ props.summary.todayUnqualifiedQuantity;
+ return total === 0 ? 0 : (props.summary.todayQualifiedQuantity / total) * 100;
+});
+
+const equipmentRate = computed(() => {
+ if (props.summary.machineryTotal === 0) return 0;
+ return (props.summary.machineryProducing / props.summary.machineryTotal) * 100;
+});
+
+/** 3D 鍊炬枩鏁堟灉 */
+interface TiltState { x: number; y: number }
+
+const tilts = ref<Record<string, TiltState>>({});
+
+function onMouseMove(e: MouseEvent, id: string) {
+ const el = e.currentTarget as HTMLElement;
+ const rect = el.getBoundingClientRect();
+ const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
+ const y = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
+ tilts.value[id] = { x: x * -6, y: y * 6 };
+}
+
+function onMouseLeave(id: string) {
+ tilts.value[id] = { x: 0, y: 0 };
+}
+
+function tiltStyle(id: string) {
+ const t = tilts.value[id] ?? { x: 0, y: 0 };
+ return {
+ transform: `perspective(600px) rotateX(${t.y}deg) rotateY(${t.x}deg)`,
+ transition: t.x === 0 && t.y === 0 ? 'transform 0.5s ease' : 'transform 0.1s ease',
+ };
+}
+</script>
+
+<template>
+ <Row :gutter="16">
+ <!-- 鐢熶骇璁㈠崟 -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('order')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'order')"
+ @mouseleave="onMouseLeave('order')"
+ @click="emit('navigate', 'MesProWorkOrder')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-cyan-400 shadow-lg shadow-blue-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:clipboard-list" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">鐢熶骇璁㈠崟</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-2xl font-bold leading-tight text-blue-600 dark:text-blue-400" :end-val="summary.workOrderActiveCount" :duration="1200" />
+ <span class="text-xs text-gray-400">杩涜涓�</span>
+ </div>
+ <div class="mt-1 flex items-center gap-1 text-xs text-gray-400">
+ <span class="inline-block size-1.5 rounded-full bg-amber-400" /> 寰呮帓浜� {{ summary.workOrderPrepareCount }}
+ <span class="mx-1">|</span>
+ <span class="inline-block size-1.5 rounded-full bg-emerald-400" /> 宸插畬鎴� {{ summary.workOrderFinishedCount }}
+ </div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-blue-500/5 to-cyan-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-blue-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+
+ <!-- 浠婃棩浜ч噺 -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('output')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'output')"
+ @mouseleave="onMouseLeave('output')"
+ @click="emit('navigate', 'MesProFeedback')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-500 to-green-400 shadow-lg shadow-emerald-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:trending-up" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">浠婃棩浜ч噺</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-2xl font-bold leading-tight text-emerald-600 dark:text-emerald-400" :end-val="summary.todayOutput" :duration="1200" />
+ <span class="text-xs text-gray-400">浠�</span>
+ </div>
+ <div class="mt-1 text-xs text-gray-400">
+ 鏄ㄦ棩 {{ summary.yesterdayOutput }} 浠�
+ <span v-if="summary.yesterdayOutput > 0 && summary.todayOutput >= summary.yesterdayOutput" class="text-emerald-500">鈫�</span>
+ <span v-else-if="summary.yesterdayOutput > 0" class="text-red-500">鈫�</span>
+ </div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-emerald-500/5 to-green-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-emerald-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+
+ <!-- 璐ㄩ噺鍚堟牸鐜� -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('quality')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')"
+ @mouseleave="onMouseLeave('quality')"
+ @click="emit('navigate', 'MesQcRecord')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:shield-check" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">璐ㄩ噺鍚堟牸鐜�</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo
+ class="text-2xl font-bold leading-tight"
+ :class="qualityRate >= 95 ? 'text-emerald-600 dark:text-emerald-400' : qualityRate >= 80 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400'"
+ :decimals="1" :end-val="qualityRate" :duration="1200"
+ />
+ <span class="text-xs text-gray-400">%</span>
+ </div>
+ <div class="mt-1 text-xs text-gray-400">
+ 鍚堟牸 {{ summary.todayQualifiedQuantity }} | 涓嶈壇 {{ summary.todayUnqualifiedQuantity }}
+ </div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-orange-500/5 to-amber-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-orange-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+
+ <!-- 璁惧杩愯鐜� -->
+ <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
+ <div
+ class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
+ :style="tiltStyle('equip')"
+ @mousemove="(e: MouseEvent) => onMouseMove(e, 'equip')"
+ @mouseleave="onMouseLeave('equip')"
+ @click="emit('navigate', 'MesDvMachinery')"
+ >
+ <div class="relative z-10 flex items-center gap-4 p-5">
+ <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-purple-400 shadow-lg shadow-violet-500/25 transition-transform duration-300 group-hover:scale-110">
+ <IconifyIcon class="size-6 text-white" icon="lucide:cpu" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">璁惧杩愯鐜�</div>
+ <div class="flex items-baseline gap-1">
+ <CountTo class="text-2xl font-bold leading-tight text-violet-600 dark:text-violet-400" :decimals="1" :end-val="equipmentRate" :duration="1200" />
+ <span class="text-xs text-gray-400">%</span>
+ </div>
+ <div class="mt-1 flex items-center gap-1 text-xs text-gray-400">
+ <span class="text-red-400">鍋滄満 {{ summary.machineryStop }}</span>
+ <span class="mx-1">|</span>
+ <span class="text-amber-400">缁存姢 {{ summary.machineryMaintenance }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="absolute inset-0 -z-0 bg-gradient-to-br from-violet-500/5 to-purple-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
+ <div class="absolute -right-6 -top-6 size-24 rounded-full bg-violet-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
+ </div>
+ </Col>
+ </Row>
+</template>
+
+<style scoped>
+.glass-tilt {
+ background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55));
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255,255,255,0.6);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.04),
+ 0 1px 4px rgba(0,0,0,0.02),
+ inset 0 1px 0 rgba(255,255,255,0.8);
+ transform-style: preserve-3d;
+}
+
+.dark .glass-tilt {
+ background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5));
+ border: 1px solid rgba(255,255,255,0.08);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.3),
+ 0 1px 4px rgba(0,0,0,0.2),
+ inset 0 1px 0 rgba(255,255,255,0.04);
+}
+</style>
diff --git a/src/views/dashboard/analytics/modules/production-trend.vue b/src/views/dashboard/analytics/modules/production-trend.vue
new file mode 100644
index 0000000..e6ad064
--- /dev/null
+++ b/src/views/dashboard/analytics/modules/production-trend.vue
@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import type { MesHomeApi } from '#/api/mes/home';
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
+import { onMounted, ref, watch } from 'vue';
+
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { getProductionTrend } from '#/api/mes/home';
+
+defineOptions({ name: 'DashboardProductionTrend' });
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+async function loadTrend() {
+ const data = await getProductionTrend(7);
+ const dates = data.map((d: MesHomeApi.ProductionTrend) => d.date.slice(5));
+ const quantities = data.map((d: MesHomeApi.ProductionTrend) => d.quantity);
+ const qualified = data.map((d: MesHomeApi.ProductionTrend) => d.qualifiedQuantity);
+ const unqualified = data.map((d: MesHomeApi.ProductionTrend) => d.unqualifiedQuantity);
+
+ renderEcharts({
+ grid: { bottom: 30, containLabel: true, left: 10, right: 30, top: 30 },
+ legend: { bottom: 0, data: ['浜ч噺', '鍚堟牸鍝�', '涓嶈壇鍝�'], textStyle: { fontSize: 12 } },
+ tooltip: { trigger: 'axis' },
+ xAxis: {
+ axisTick: { show: false },
+ boundaryGap: false,
+ data: dates,
+ type: 'category',
+ },
+ yAxis: {
+ axisTick: { show: false },
+ splitNumber: 4,
+ type: 'value',
+ },
+ series: [
+ {
+ barMaxWidth: 32,
+ data: quantities,
+ itemStyle: {
+ borderRadius: 4,
+ color: '#2563eb',
+ },
+ name: '浜ч噺',
+ type: 'bar',
+ },
+ {
+ data: qualified,
+ itemStyle: { color: '#16a34a' },
+ name: '鍚堟牸鍝�',
+ smooth: true,
+ symbol: 'circle',
+ symbolSize: 6,
+ type: 'line',
+ },
+ {
+ data: unqualified,
+ itemStyle: { color: '#dc2626' },
+ name: '涓嶈壇鍝�',
+ smooth: true,
+ symbol: 'diamond',
+ symbolSize: 6,
+ type: 'line',
+ },
+ ],
+ });
+}
+
+onMounted(() => {
+ loadTrend();
+});
+</script>
+
+<template>
+ <EchartsUI ref="chartRef" class="!h-[320px]" />
+</template>
diff --git a/src/views/dashboard/analytics/modules/quick-actions.vue b/src/views/dashboard/analytics/modules/quick-actions.vue
new file mode 100644
index 0000000..f84affb
--- /dev/null
+++ b/src/views/dashboard/analytics/modules/quick-actions.vue
@@ -0,0 +1,73 @@
+<script lang="ts" setup>
+import { IconifyIcon } from '@vben/icons';
+
+defineOptions({ name: 'DashboardQuickActions' });
+
+const emit = defineEmits<{
+ navigate: [name: string];
+}>();
+
+interface QuickAction {
+ title: string;
+ icon: string;
+ route: string;
+ gradient: string;
+ shadow: string;
+}
+
+const actions: QuickAction[] = [
+ { title: '鐢熶骇宸ュ崟', icon: 'lucide:file-text', route: 'MesProWorkOrder', gradient: 'from-blue-500 to-cyan-500', shadow: 'shadow-blue-500/15' },
+ { title: '鐢熶骇鎶ュ伐', icon: 'lucide:check-square', route: 'MesProFeedback', gradient: 'from-emerald-500 to-green-500', shadow: 'shadow-emerald-500/15' },
+ { title: '璐ㄦ绠$悊', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' },
+ { title: '璁惧鐐规', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' },
+ { title: '鐢熶骇鎺掔▼', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' },
+ { title: '瀹夌伅鍛煎彨', icon: 'lucide:bell-ring', route: 'MesProAndon', gradient: 'from-red-500 to-rose-500', shadow: 'shadow-red-500/15' },
+ { title: '鐗╂枡搴撳瓨', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' },
+ { title: '宸ヨ壓璺嚎', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' },
+ { title: '鏁版嵁鎶ヨ〃', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' },
+];
+</script>
+
+<template>
+ <div class="glass-card rounded-2xl p-5">
+ <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">蹇嵎鎿嶄綔</h3>
+ <div class="grid grid-cols-3 gap-3 sm:grid-cols-9">
+ <div
+ v-for="action in actions"
+ :key="action.route"
+ class="group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-all duration-300 hover:-translate-y-1"
+ @click="emit('navigate', action.route)"
+ >
+ <div
+ class="flex size-10 items-center justify-center rounded-xl bg-gradient-to-br transition-shadow duration-300 group-hover:shadow-lg"
+ :class="[action.gradient, action.shadow]"
+ >
+ <IconifyIcon :icon="action.icon" class="size-5 text-white" />
+ </div>
+ <span class="text-xs font-medium text-gray-600 dark:text-gray-400">{{ action.title }}</span>
+ </div>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.glass-card {
+ background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55));
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255,255,255,0.6);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.04),
+ 0 1px 4px rgba(0,0,0,0.02),
+ inset 0 1px 0 rgba(255,255,255,0.8);
+}
+
+.dark .glass-card {
+ background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5));
+ border: 1px solid rgba(255,255,255,0.08);
+ box-shadow:
+ 0 4px 24px rgba(0,0,0,0.3),
+ 0 1px 4px rgba(0,0,0,0.2),
+ inset 0 1px 0 rgba(255,255,255,0.04);
+}
+</style>
--
Gitblit v1.9.3