feat(dashboard): 重构分析仪表盘页面并移除旧图表组件
- 移除旧的流量趋势、访问量等图表组件文件
- 新增欢迎栏显示动态问候语和系统状态
- 集成生产KPI卡片展示生产订单、产量、质量等核心指标
- 添加经营KPI卡片显示采购、销售、库存、待办数据
- 创建预警面板显示安灯呼叫和维修工单状态
- 实现生产趋势图表展示近7天生产情况
- 添加快捷操作区域便于快速导航到各功能模块
- 使用玻璃态设计风格提升界面视觉效果
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | { |
| | | "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": [] |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }; |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |