From f7072c928b7cb9c8ae3bdba00e6c046e3f6f1c34 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 14 十月 2026 14:34:40 +0800
Subject: [PATCH] feat(首页): 新增仓储物流与人力资源概览卡片

---
 src/views/dashboard/analytics/modules/warehouse-hr-cards.vue |  298 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/views/dashboard/analytics/index.vue                      |    4 
 2 files changed, 302 insertions(+), 0 deletions(-)

diff --git a/src/views/dashboard/analytics/index.vue b/src/views/dashboard/analytics/index.vue
index cdf7447..9745151 100644
--- a/src/views/dashboard/analytics/index.vue
+++ b/src/views/dashboard/analytics/index.vue
@@ -17,6 +17,7 @@
 import KpiCards from './modules/kpi-cards.vue';
 import ProductionTrend from './modules/production-trend.vue';
 import QuickActions from './modules/quick-actions.vue';
+import WarehouseHrCards from './modules/warehouse-hr-cards.vue';
 
 defineOptions({ name: 'DashboardAnalytics' });
 
@@ -104,6 +105,9 @@
     <!-- ERP/WMS/BPM 缁忚惀 KPI -->
     <BizCards @navigate="handleNavigate" />
 
+    <!-- 浠撳偍鐗╂祦 / 浜哄姏璧勬簮涓氬姟姒傝锛堣彍鍗曟病寮�鐨勬暣缁勪笉灞曠ず锛� -->
+    <WarehouseHrCards @navigate="handleNavigate" />
+
     <!-- AI 鏅鸿兘鍒嗘瀽 -->
     <AiInsights
       v-if="showAiInsights"
diff --git a/src/views/dashboard/analytics/modules/warehouse-hr-cards.vue b/src/views/dashboard/analytics/modules/warehouse-hr-cards.vue
new file mode 100644
index 0000000..bd08fe2
--- /dev/null
+++ b/src/views/dashboard/analytics/modules/warehouse-hr-cards.vue
@@ -0,0 +1,298 @@
+<script lang="ts" setup>
+import { computed, onMounted, ref } from 'vue';
+
+import { CountTo } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Col, Row } from 'ant-design-vue';
+
+import { getAttendanceExceptionPage } from '#/api/hrm/attendance/exception';
+import { getEmployeePage } from '#/api/hrm/employee';
+import { getLeavePage } from '#/api/hrm/leave';
+import { getSalaryPaymentPage } from '#/api/hrm/salary/payment';
+import { getItemReceiptPage } from '#/api/mes/wm/itemreceipt';
+import { getMaterialStockPage } from '#/api/mes/wm/materialstock';
+import { getProductIssuePage } from '#/api/mes/wm/productissue';
+import { getWarehousePage } from '#/api/mes/wm/warehouse';
+
+import { useHomeAccess } from '#/utils/use-home-access';
+
+defineOptions({ name: 'DashboardWarehouseHrCards' });
+
+const emit = defineEmits<{
+  navigate: [name: string];
+}>();
+
+/** 缁熻鍗$墖瀹氫箟锛氬彇鏁板彧璋冨悇鑷垪琛ㄦ帴鍙f嬁鎬绘暟锛岃彍鍗曟病寮�鐨勫崱涓嶆樉绀� */
+interface StatCard {
+  /** 鍗$墖鏍囬 */
+  label: string;
+  /** 鍗曚綅 */
+  unit: string;
+  /** 鍓爣棰橈細鏁版嵁鏉ユ簮 */
+  hint: string;
+  icon: string;
+  /** 鍥炬爣搴曡壊娓愬彉锛圱ailwind锛� */
+  gradient: string;
+  /** 鏁板�奸鑹诧紙Tailwind锛� */
+  color: string;
+  /** 鍗$墖褰掑睘鐨勮彍鍗曪紙璺敱 name锛夛紝鍏崇潃灏变笉鐢ㄥ睍绀� */
+  menu: string;
+  /** 鑿滃崟瀵瑰簲鐨勬煡璇㈡潈闄愮爜锛屾病鏈夋寜閽潈闄愮殑鑿滃崟鐣欑┖ */
+  permission?: string;
+  /** 鍙栨暟鏂规硶 */
+  load: () => Promise<number>;
+}
+
+/** 鏌ヨ鍙傛暟锛氬彧瑕� total锛屽垎椤靛ぇ灏忓彇 1锛岄伩鍏嶆媺鍥炴暣椤垫槑缁� */
+const pageParam = { pageNo: 1, pageSize: 1 };
+
+/** 浠庡垎椤电粨鏋滈噷鍙栨�绘暟 */
+async function totalOf(promise: Promise<{ total?: number }>) {
+  const result = await promise;
+  return result.total ?? 0;
+}
+
+/** 浠撳偍鐗╂祦 */
+const WAREHOUSE_CARDS: StatCard[] = [
+  {
+    color: 'text-amber-600 dark:text-amber-400',
+    gradient: 'from-amber-500 to-yellow-400',
+    hint: '浠撳偍鐗╂祦 路 搴撳瓨鐜版湁閲�',
+    icon: 'lucide:package-search',
+    label: '搴撳瓨鐗╂枡',
+    load: () => totalOf(getMaterialStockPage(pageParam)),
+    menu: 'MesWmMaterialStock',
+    permission: 'mes:wm-material-stock:query',
+    unit: '鏉�',
+  },
+  {
+    color: 'text-blue-600 dark:text-blue-400',
+    gradient: 'from-blue-500 to-indigo-400',
+    hint: '浠撳偍鐗╂祦 路 浠撳簱璁剧疆',
+    icon: 'lucide:warehouse',
+    label: '浠撳簱鏁伴噺',
+    load: () => totalOf(getWarehousePage(pageParam)),
+    menu: 'MesWmWarehouse',
+    permission: 'mes:wm-warehouse:query',
+    unit: '涓�',
+  },
+  {
+    color: 'text-cyan-600 dark:text-cyan-400',
+    gradient: 'from-cyan-500 to-teal-400',
+    hint: '浠撳偍鐗╂祦 路 閲囪喘鍏ュ簱',
+    icon: 'lucide:package-open',
+    label: '閲囪喘鍏ュ簱鍗�',
+    load: () => totalOf(getItemReceiptPage(pageParam)),
+    menu: 'MesWmItemReceipt',
+    permission: 'mes:wm-item-receipt:query',
+    unit: '绗�',
+  },
+  {
+    color: 'text-violet-600 dark:text-violet-400',
+    gradient: 'from-violet-500 to-purple-400',
+    hint: '浠撳偍鐗╂祦 路 鐢熶骇棰嗘枡',
+    icon: 'lucide:package',
+    label: '鐢熶骇棰嗘枡鍗�',
+    load: () => totalOf(getProductIssuePage(pageParam)),
+    menu: 'MesWmProductIssue',
+    permission: 'mes:wm-product-issue:query',
+    unit: '绗�',
+  },
+];
+
+/** 浜哄姏璧勬簮 */
+const HR_CARDS: StatCard[] = [
+  {
+    color: 'text-emerald-600 dark:text-emerald-400',
+    gradient: 'from-emerald-500 to-green-400',
+    hint: '浜哄姏璧勬簮 路 鍛樺伐绠$悊',
+    icon: 'lucide:users',
+    label: '鍛樺伐鎬绘暟',
+    load: () => totalOf(getEmployeePage(pageParam)),
+    menu: 'HrmEmployee',
+    permission: 'hrm:employee:query',
+    unit: '浜�',
+  },
+  {
+    color: 'text-orange-600 dark:text-orange-400',
+    gradient: 'from-orange-500 to-amber-400',
+    hint: '浜哄姏璧勬簮 路 璇峰亣鐢宠',
+    icon: 'lucide:calendar-range',
+    label: '璇峰亣鐢宠',
+    load: () => totalOf(getLeavePage(pageParam)),
+    menu: 'HrmLeaveApplication',
+    permission: 'hrm:leave:query',
+    unit: '鍗�',
+  },
+  {
+    color: 'text-rose-600 dark:text-rose-400',
+    gradient: 'from-rose-500 to-pink-400',
+    hint: '浜哄姏璧勬簮 路 鑰冨嫟寮傚父',
+    icon: 'lucide:circle-alert',
+    label: '鑰冨嫟寮傚父',
+    load: () => totalOf(getAttendanceExceptionPage(pageParam)),
+    menu: 'HrmAttendanceException',
+    unit: '鏉�',
+  },
+  {
+    color: 'text-fuchsia-600 dark:text-fuchsia-400',
+    gradient: 'from-fuchsia-500 to-pink-400',
+    hint: '浜哄姏璧勬簮 路 钖祫鍙戞斁',
+    icon: 'lucide:wallet',
+    label: '钖祫鍙戞斁鍗�',
+    load: () => totalOf(getSalaryPaymentPage(pageParam)),
+    menu: 'HrmSalaryPayment',
+    unit: '鍗�',
+  },
+];
+
+const { canQuery, canSeeMenu } = useHomeAccess();
+
+/** 鑿滃崟寮�鐫�锛堟湁鏌ヨ鎸夐挳鏉冮檺鏃朵竴骞舵牎楠岋級鎵嶅睍绀鸿繖寮犲崱 */
+function isVisible(card: StatCard) {
+  return (
+    canSeeMenu(card.menu) && (!card.permission || canQuery(card.permission))
+  );
+}
+
+const warehouseCards = computed(() => WAREHOUSE_CARDS.filter(isVisible));
+const hrCards = computed(() => HR_CARDS.filter(isVisible));
+
+/** 鎸夊垎缁勬覆鏌擄紝鍒嗙粍鍐呮棤鍗$墖鏃舵暣缁勪笉娓叉煋 */
+const groups = computed(() =>
+  [
+    { cards: warehouseCards.value, title: '浠撳偍鐗╂祦' },
+    { cards: hrCards.value, title: '浜哄姏璧勬簮' },
+  ].filter((group) => group.cards.length > 0),
+);
+
+const values = ref<Record<string, number>>({});
+
+/** 骞跺彂鎷夊彇鎵�鏈夊彲瑙佸崱鐗囩殑缁熻鍊硷紝鍗曞紶澶辫触鍙奖鍝嶅畠鑷繁 */
+async function loadValues() {
+  const cards = [...warehouseCards.value, ...hrCards.value];
+  await Promise.all(
+    cards.map(async (card) => {
+      try {
+        values.value[card.label] = await card.load();
+      } catch {
+        /* 鏃犳潈闄愭垨鎺ュ彛寮傚父鏃朵繚鎸� 0 */
+      }
+    }),
+  );
+}
+
+/** 3D 鍊炬枩 */
+const tilts = ref<Record<string, { x: number; y: number }>>({});
+
+function onMouseMove(e: MouseEvent, label: 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[label] = { x: x * -5, y: y * 5 };
+}
+
+function onMouseLeave(label: string) {
+  tilts.value[label] = { x: 0, y: 0 };
+}
+
+function tiltStyle(label: string) {
+  const t = tilts.value[label] ?? { 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(() => {
+  loadValues();
+});
+</script>
+
+<template>
+  <template v-for="group in groups" :key="group.title">
+    <h3
+      class="mb-3 text-base font-semibold text-gray-800 dark:text-gray-100"
+    >
+      {{ group.title }}
+    </h3>
+    <Row :gutter="16">
+      <Col
+        v-for="card in group.cards"
+        :key="card.label"
+        :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(card.label)"
+          @mousemove="(e: MouseEvent) => onMouseMove(e, card.label)"
+          @mouseleave="onMouseLeave(card.label)"
+          @click="emit('navigate', card.menu)"
+        >
+          <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 shadow-lg transition-transform duration-300 group-hover:scale-110"
+              :class="card.gradient"
+            >
+              <IconifyIcon class="size-6 text-white" :icon="card.icon" />
+            </div>
+            <div class="min-w-0 flex-1">
+              <div
+                class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400"
+              >
+                {{ card.label }}
+              </div>
+              <div class="flex items-baseline gap-1">
+                <CountTo
+                  class="text-2xl font-bold leading-tight"
+                  :class="card.color"
+                  :end-val="values[card.label] ?? 0"
+                  :duration="1200"
+                />
+                <span class="text-xs text-gray-400">{{ card.unit }}</span>
+              </div>
+              <div class="mt-1 text-xs text-gray-400">{{ card.hint }}</div>
+            </div>
+          </div>
+          <div
+            class="absolute inset-0 -z-0 bg-gradient-to-br from-gray-500/5 to-gray-400/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
+          />
+          <div
+            class="absolute -right-6 -top-6 size-24 rounded-full bg-gray-400/10 blur-xl transition-all duration-300 group-hover:scale-150"
+          />
+        </div>
+      </Col>
+    </Row>
+  </template>
+</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 2px 8px rgba(0,0,0,0.2),
+    inset 0 1px 0 rgba(255,255,255,0.05);
+}
+</style>

--
Gitblit v1.9.3