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