| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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]; |
| | | }>(); |
| | | |
| | | /** ç»è®¡å¡çå®ä¹ï¼åæ°åªè°åèªå表æ¥å£æ¿æ»æ°ï¼èåæ²¡å¼çå¡ä¸æ¾ç¤º */ |
| | | interface StatCard { |
| | | /** å¡çæ é¢ */ |
| | | label: string; |
| | | /** åä½ */ |
| | | unit: string; |
| | | /** 坿 é¢ï¼æ°æ®æ¥æº */ |
| | | hint: string; |
| | | icon: string; |
| | | /** 徿 åºè²æ¸åï¼Tailwindï¼ */ |
| | | 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> |