2026-08-03 c4a2d727fb1b72a1b7b7ae8c0d778f0e81952cce
feat(dashboard): 重构分析仪表盘页面并移除旧图表组件

- 移除旧的流量趋势、访问量等图表组件文件
- 新增欢迎栏显示动态问候语和系统状态
- 集成生产KPI卡片展示生产订单、产量、质量等核心指标
- 添加经营KPI卡片显示采购、销售、库存、待办数据
- 创建预警面板显示安灯呼叫和维修工单状态
- 实现生产趋势图表展示近7天生产情况
- 添加快捷操作区域便于快速导航到各功能模块
- 使用玻璃态设计风格提升界面视觉效果
已修改1个文件
已添加7个文件
已删除5个文件
1413 ■■■■■ 文件已修改
docs/dashboard-layout.canvas 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/analytics-trends.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/analytics-visits-data.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/analytics-visits-sales.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/analytics-visits-source.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/analytics-visits.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/data.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/index.vue 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/alert-panel.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/biz-cards.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/kpi-cards.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/production-trend.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/quick-actions.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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": []
}
src/views/dashboard/analytics/analytics-trends.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/analytics-visits-data.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/analytics-visits-sales.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/analytics-visits-source.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/analytics-visits.vue
ÎļþÒÑɾ³ý
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,
};
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>
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>
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>
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>
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>
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>