| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Col, Progress, Row, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { getAttendanceSummaryPage } from '#/api/hrm/attendance/summary'; |
| | | import { getMaterialStockPage } from '#/api/mes/wm/materialstock'; |
| | | import { getPurchaseOrderPage } from '#/api/erp/purchase/order'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardHrStockCards' }); |
| | | |
| | | const emit = defineEmits<{ navigate: [name: string] }>(); |
| | | |
| | | const { canNavigate, canQuery } = useHomeAccess(); |
| | | const canViewHr = computed(() => canQuery('hrm:attendance-summary:query') || canQuery('hrm:attendance-record:query')); |
| | | const canViewStock = computed(() => canQuery('mes:wm-material-stock:query') && canNavigate('MesWmMaterialStock')); |
| | | const canViewPurchase = computed(() => canQuery('erp:purchase-order:query') && canNavigate('ErpPurchaseOrder')); |
| | | |
| | | // ========== åºå¤ææ ========== |
| | | const hrLoading = ref(false); |
| | | const headCount = ref(0); |
| | | const actualDaysAvg = ref(0); |
| | | const attendanceRate = ref(0); |
| | | const lateCount = ref(0); |
| | | const absentCount = ref(0); |
| | | const leaveHours = ref(0); |
| | | |
| | | function currentYearMonth() { |
| | | const d = new Date(); |
| | | return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; |
| | | } |
| | | |
| | | async function loadAttendance() { |
| | | if (!canViewHr.value) return; |
| | | hrLoading.value = true; |
| | | try { |
| | | const res = await getAttendanceSummaryPage({ |
| | | pageNo: 1, |
| | | pageSize: 500, |
| | | yearMonth: currentYearMonth(), |
| | | } as any); |
| | | const list = res?.list ?? []; |
| | | headCount.value = res?.total ?? list.length; |
| | | if (list.length > 0) { |
| | | let workSum = 0, actualSum = 0, late = 0, absent = 0, leave = 0; |
| | | for (const r of list) { |
| | | workSum += Number(r.workDays ?? 0); |
| | | actualSum += Number(r.actualDays ?? 0); |
| | | late += Number(r.lateCount ?? 0); |
| | | absent += Number(r.absentCount ?? 0); |
| | | leave += Number(r.leaveHours ?? 0); |
| | | } |
| | | actualDaysAvg.value = workSum > 0 ? Number((actualSum / list.length).toFixed(1)) : 0; |
| | | attendanceRate.value = workSum > 0 ? Math.min(100, Math.round((actualSum / workSum) * 100)) : 0; |
| | | lateCount.value = late; |
| | | absentCount.value = absent; |
| | | leaveHours.value = Number(leave.toFixed(1)); |
| | | } |
| | | } catch { /* å¿½ç¥ */ } finally { hrLoading.value = false; } |
| | | } |
| | | |
| | | // ========== åºåéè´ ========== |
| | | const stockLoading = ref(false); |
| | | const stockKinds = ref(0); |
| | | const stockAvailable = ref(0); |
| | | const purchaseCount = ref(0); |
| | | const purchasePending = ref(0); |
| | | |
| | | async function loadStock() { |
| | | if (!canViewStock.value) return; |
| | | stockLoading.value = true; |
| | | try { |
| | | const res = await getMaterialStockPage({ pageNo: 1, pageSize: 500 } as any); |
| | | const list = res?.list ?? []; |
| | | stockKinds.value = res?.total ?? list.length; |
| | | stockAvailable.value = Number(list.reduce((s: number, r: any) => s + Number(r.availableQuantity ?? 0), 0).toFixed(0)); |
| | | } catch { /* å¿½ç¥ */ } finally { stockLoading.value = false; } |
| | | } |
| | | |
| | | async function loadPurchase() { |
| | | if (!canViewPurchase.value) return; |
| | | try { |
| | | const res = await getPurchaseOrderPage({ pageNo: 1, pageSize: 1 } as any); |
| | | purchaseCount.value = res?.total ?? 0; |
| | | // å¾
å®¡æ¹æ°éï¼ç¶æ 10 = 审æ¹ä¸ï¼æ yudao ERP 约å®ï¼ |
| | | const pending = await getPurchaseOrderPage({ pageNo: 1, pageSize: 1, status: 10 } as any); |
| | | purchasePending.value = pending?.total ?? 0; |
| | | } catch { /* å¿½ç¥ */ } |
| | | } |
| | | |
| | | function goto(name: string) { |
| | | if (canNavigate(name)) emit('navigate', name); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadAttendance(); |
| | | loadStock(); |
| | | loadPurchase(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Row :gutter="[16, 16]"> |
| | | <!-- ============ åºå¤ææ ============ --> |
| | | <Col v-if="canViewHr" :xs="24" :sm="24" :md="24" :lg="12" :xl="12" :xxl="12"> |
| | | <div class="glass-card h-full overflow-hidden rounded-2xl p-5"> |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <div class="flex items-center gap-2"> |
| | | <div class="flex size-9 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-400 shadow-md shadow-violet-500/25"> |
| | | <IconifyIcon class="size-5 text-white" icon="lucide:user-check" /> |
| | | </div> |
| | | <div> |
| | | <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">åºå¤ææ </h3> |
| | | <p class="text-xs text-gray-400">æ¬æè夿±æ»</p> |
| | | </div> |
| | | </div> |
| | | <a-spin v-if="hrLoading" :size="16" /> |
| | | </div> |
| | | |
| | | <div class="mb-4 flex items-center gap-5"> |
| | | <Progress |
| | | type="circle" |
| | | :percent="attendanceRate" |
| | | :size="88" |
| | | :stroke-color="{ '0%': '#8b5cf6', '100%': '#c084fc' }" |
| | | /> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="text-xs text-gray-500 dark:text-gray-400">åºå¤ç</div> |
| | | <div class="mt-1 text-sm text-gray-600 dark:text-gray-300"> |
| | | å¨å <span class="font-semibold text-violet-600 dark:text-violet-400">{{ headCount }}</span> 人 |
| | | · 人ååºå¤ <span class="font-semibold text-violet-600 dark:text-violet-400">{{ actualDaysAvg }}</span> 天 |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="grid grid-cols-2 gap-3 sm:grid-cols-4"> |
| | | <Tooltip title="æ¬æè¿å°æ¬¡æ°å计"> |
| | | <div class="rounded-xl bg-amber-50/70 p-3 text-center dark:bg-amber-500/10"> |
| | | <div class="text-lg font-bold text-amber-600 dark:text-amber-400"> |
| | | <CountTo :end-val="lateCount" :duration="1000" /> |
| | | </div> |
| | | <div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">è¿å°</div> |
| | | </div> |
| | | </Tooltip> |
| | | <Tooltip title="æ¬æç¼ºå¡æ¬¡æ°å计"> |
| | | <div class="rounded-xl bg-rose-50/70 p-3 text-center dark:bg-rose-500/10"> |
| | | <div class="text-lg font-bold text-rose-600 dark:text-rose-400"> |
| | | <CountTo :end-val="absentCount" :duration="1000" /> |
| | | </div> |
| | | <div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">缺å¡</div> |
| | | </div> |
| | | </Tooltip> |
| | | <Tooltip title="æ¬æè¯·åå·¥æ¶å计"> |
| | | <div class="rounded-xl bg-sky-50/70 p-3 text-center dark:bg-sky-500/10"> |
| | | <div class="text-lg font-bold text-sky-600 dark:text-sky-400"> |
| | | <CountTo :end-val="leaveHours" :duration="1000" :decimals="1" /> |
| | | </div> |
| | | <div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">请å(h)</div> |
| | | </div> |
| | | </Tooltip> |
| | | <Tooltip title="人ååºå¤å¤©æ°"> |
| | | <div class="rounded-xl bg-emerald-50/70 p-3 text-center dark:bg-emerald-500/10"> |
| | | <div class="text-lg font-bold text-emerald-600 dark:text-emerald-400"> |
| | | <CountTo :end-val="actualDaysAvg" :duration="1000" :decimals="1" /> |
| | | </div> |
| | | <div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">åºå¤(天)</div> |
| | | </div> |
| | | </Tooltip> |
| | | </div> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- ============ åºåéè´ ============ --> |
| | | <Col v-if="canViewStock || canViewPurchase" :xs="24" :sm="24" :md="24" :lg="12" :xl="12" :xxl="12"> |
| | | <div class="glass-card h-full overflow-hidden rounded-2xl p-5"> |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <div class="flex items-center gap-2"> |
| | | <div class="flex size-9 items-center justify-center rounded-lg bg-gradient-to-br from-cyan-500 to-teal-400 shadow-md shadow-cyan-500/25"> |
| | | <IconifyIcon class="size-5 text-white" icon="lucide:package" /> |
| | | </div> |
| | | <div> |
| | | <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100">åºåéè´</h3> |
| | | <p class="text-xs text-gray-400">ä»å¨ä¸éè´æ¦è§</p> |
| | | </div> |
| | | </div> |
| | | <a-spin v-if="stockLoading" :size="16" /> |
| | | </div> |
| | | |
| | | <Row :gutter="[12, 12]"> |
| | | <Col v-if="canViewStock" :xs="12" :sm="12" :md="12" :lg="12" :xl="12" class="cursor-pointer" @click="goto('MesWmMaterialStock')"> |
| | | <div class="rounded-xl bg-cyan-50/70 p-4 transition hover:bg-cyan-100/70 dark:bg-cyan-500/10"> |
| | | <div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400"> |
| | | <IconifyIcon class="size-4 text-cyan-500" icon="lucide:boxes" /> åºååç§ |
| | | </div> |
| | | <div class="mt-1 text-2xl font-bold text-cyan-600 dark:text-cyan-400"> |
| | | <CountTo :end-val="stockKinds" :duration="1000" /> |
| | | </div> |
| | | <div class="text-xs text-gray-400">累计å¨åºç©æè¡</div> |
| | | </div> |
| | | </Col> |
| | | <Col v-if="canViewStock" :xs="12" :sm="12" :md="12" :lg="12" :xl="12"> |
| | | <div class="rounded-xl bg-teal-50/70 p-4 dark:bg-teal-500/10"> |
| | | <div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400"> |
| | | <IconifyIcon class="size-4 text-teal-500" icon="lucide:warehouse" /> å¯ç¨æ°é |
| | | </div> |
| | | <div class="mt-1 text-2xl font-bold text-teal-600 dark:text-teal-400"> |
| | | <CountTo :end-val="stockAvailable" :duration="1000" /> |
| | | </div> |
| | | <div class="text-xs text-gray-400">æ£é¤å»ç»ä¸å ç¨</div> |
| | | </div> |
| | | </Col> |
| | | <Col v-if="canViewPurchase" :xs="12" :sm="12" :md="12" :lg="12" :xl="12" class="cursor-pointer" @click="goto('ErpPurchaseOrder')"> |
| | | <div class="rounded-xl bg-indigo-50/70 p-4 transition hover:bg-indigo-100/70 dark:bg-indigo-500/10"> |
| | | <div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400"> |
| | | <IconifyIcon class="size-4 text-indigo-500" icon="lucide:shopping-cart" /> éè´è®¢å |
| | | </div> |
| | | <div class="mt-1 text-2xl font-bold text-indigo-600 dark:text-indigo-400"> |
| | | <CountTo :end-val="purchaseCount" :duration="1000" /> |
| | | </div> |
| | | <div class="text-xs text-gray-400">累计订åç¬æ°</div> |
| | | </div> |
| | | </Col> |
| | | <Col v-if="canViewPurchase" :xs="12" :sm="12" :md="12" :lg="12" :xl="12"> |
| | | <div class="rounded-xl bg-orange-50/70 p-4 dark:bg-orange-500/10"> |
| | | <div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400"> |
| | | <IconifyIcon class="size-4 text-orange-500" icon="lucide:clock" /> å¾
å®¡æ¹ |
| | | </div> |
| | | <div class="mt-1 text-2xl font-bold text-orange-600 dark:text-orange-400"> |
| | | <CountTo :end-val="purchasePending" :duration="1000" /> |
| | | </div> |
| | | <div class="text-xs text-gray-400">审æ¹ä¸çéè´å</div> |
| | | </div> |
| | | </Col> |
| | | </Row> |
| | | </div> |
| | | </Col> |
| | | </Row> |
| | | </template> |