| | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Col, Progress, Row, Tooltip } from 'ant-design-vue'; |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | import { getAttendanceSummaryPage } from '#/api/hrm/attendance/summary'; |
| | | import { getMaterialStockPage } from '#/api/mes/wm/materialstock'; |
| | |
| | | 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 canViewAttendance = |
| | | 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 attendanceRate = ref<number | null>(null); |
| | | const averageDays = ref(0); |
| | | const abnormalCount = ref(0); |
| | | const lateCount = ref(0); |
| | | const absentCount = ref(0); |
| | | const leaveHours = ref(0); |
| | |
| | | } |
| | | |
| | | async function loadAttendance() { |
| | | if (!canViewHr.value) return; |
| | | hrLoading.value = true; |
| | | if (!canViewAttendance.value) return; |
| | | try { |
| | | const res = await getAttendanceSummaryPage({ |
| | | pageNo: 1, |
| | | pageSize: 200, |
| | | 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) { |
| | | const rows = res?.list ?? []; |
| | | headCount.value = res?.total ?? rows.length; |
| | | if (rows.length > 0) { |
| | | let workSum = 0; |
| | | let actualSum = 0; |
| | | let late = 0; |
| | | let absent = 0; |
| | | let leave = 0; |
| | | for (const r of rows) { |
| | | 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; |
| | | averageDays.value = Number((actualSum / rows.length).toFixed(1)); |
| | | attendanceRate.value = workSum > 0 ? Number(((actualSum / workSum) * 100).toFixed(1)) : null; |
| | | lateCount.value = late; |
| | | absentCount.value = absent; |
| | | leaveHours.value = Number(leave.toFixed(1)); |
| | | abnormalCount.value = late + absent; |
| | | } |
| | | } catch { /* 忽略 */ } finally { hrLoading.value = false; } |
| | | } catch { |
| | | /* 忽略 */ |
| | | } |
| | | } |
| | | |
| | | // ========== 库存采购 ========== |
| | | const stockLoading = ref(false); |
| | | const stockKinds = ref(0); |
| | | // ========== 库存 / 采购 ========== |
| | | const stockSkuCount = ref(0); |
| | | const stockAvailable = ref(0); |
| | | const purchaseCount = ref(0); |
| | | const purchaseTotal = ref(0); |
| | | const purchasePending = ref(0); |
| | | |
| | | async function loadStock() { |
| | | if (!canViewStock.value) return; |
| | | stockLoading.value = true; |
| | | try { |
| | | const res = await getMaterialStockPage({ pageNo: 1, pageSize: 200 } 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; } |
| | | const rows = res?.list ?? []; |
| | | stockSkuCount.value = res?.total ?? rows.length; |
| | | stockAvailable.value = Math.round( |
| | | rows.reduce((sum: number, r: any) => sum + Number(r.availableQuantity ?? 0), 0), |
| | | ); |
| | | } catch { |
| | | /* 忽略 */ |
| | | } |
| | | } |
| | | |
| | | 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 约定) |
| | | purchaseTotal.value = res?.total ?? 0; |
| | | const pending = await getPurchaseOrderPage({ pageNo: 1, pageSize: 1, status: 10 } as any); |
| | | purchasePending.value = pending?.total ?? 0; |
| | | } catch { /* 忽略 */ } |
| | | } catch { |
| | | /* 忽略 */ |
| | | } |
| | | } |
| | | |
| | | function goto(name: string) { |
| | | if (canNavigate(name)) emit('navigate', name); |
| | | } |
| | | |
| | | /** 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', |
| | | }; |
| | | } |
| | | |
| | | /** 出勤率展示:无数据用「—」 */ |
| | | const attendanceRateText = computed(() => |
| | | attendanceRate.value === null ? '—' : attendanceRate.value.toFixed(1), |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | loadAttendance(); |
| | |
| | | </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> |
| | | <Row :gutter="16"> |
| | | <!-- 本月出勤率 --> |
| | | <Col v-if="canViewAttendance" :xs="24" :sm="12" :md="8" :lg="8" :xl="6" :xxl="6" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('attendance')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'attendance')" |
| | | @mouseleave="onMouseLeave('attendance')" |
| | | > |
| | | <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:user-check" /> |
| | | </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 class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">本月出勤率</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | v-if="attendanceRate !== null" |
| | | class="text-2xl font-bold leading-tight text-violet-600 dark:text-violet-400" |
| | | :end-val="attendanceRate" |
| | | :duration="1200" |
| | | :decimals="1" |
| | | /> |
| | | <span |
| | | v-else |
| | | class="text-2xl font-bold leading-tight text-gray-400 dark:text-gray-500" |
| | | >{{ attendanceRateText }}</span |
| | | > |
| | | <span v-if="attendanceRate !== null" 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-violet-400" /> 在册 {{ headCount }} |
| | | <span class="mx-1">|</span> |
| | | <span class="inline-block size-1.5 rounded-full bg-emerald-400" /> 人均 |
| | | {{ averageDays }} 天 |
| | | </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 |
| | | 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> |
| | | |
| | | <!-- ============ 库存采购 ============ --> |
| | | <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" /> |
| | | <!-- 本月考勤异常 --> |
| | | <Col v-if="canViewAttendance" :xs="24" :sm="12" :md="8" :lg="8" :xl="6" :xxl="6" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0 cursor-pointer" |
| | | :style="tiltStyle('attendance-abnormal')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'attendance-abnormal')" |
| | | @mouseleave="onMouseLeave('attendance-abnormal')" |
| | | @click="goto('HrmAttendanceRecord')" |
| | | > |
| | | <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-orange-400 shadow-lg shadow-amber-500/25 transition-transform duration-300 group-hover:scale-110" |
| | | > |
| | | <IconifyIcon class="size-6 text-white" icon="lucide:alert-triangle" /> |
| | | </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="abnormalCount" |
| | | :duration="1200" |
| | | /> |
| | | <span class="text-xs text-gray-400">次</span> |
| | | </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 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" /> 迟到 {{ lateCount }} |
| | | <span class="mx-1">|</span> |
| | | <span class="inline-block size-1.5 rounded-full bg-rose-400" /> 缺卡 {{ absentCount }} |
| | | </div> |
| | | </div> |
| | | <a-spin v-if="stockLoading" :size="16" /> |
| | | </div> |
| | | <div |
| | | class="absolute inset-0 -z-0 bg-gradient-to-br from-amber-500/5 to-orange-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> |
| | | |
| | | <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> |
| | | <!-- 库存可用量 --> |
| | | <Col v-if="canViewStock" :xs="24" :sm="12" :md="8" :lg="8" :xl="6" :xxl="6" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0 cursor-pointer" |
| | | :style="tiltStyle('stock')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'stock')" |
| | | @mouseleave="onMouseLeave('stock')" |
| | | @click="goto('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-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:package" /> |
| | | </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="stockAvailable" |
| | | :duration="1200" |
| | | /> |
| | | </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 class="mt-1 flex items-center gap-1 text-xs text-gray-400"> |
| | | <span class="inline-block size-1.5 rounded-full bg-cyan-400" /> 品种 {{ stockSkuCount }} |
| | | <span class="mx-1">|</span> 扣除冻结占用 |
| | | </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> |
| | | </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 v-if="canViewPurchase" :xs="24" :sm="12" :md="8" :lg="8" :xl="6" :xxl="6" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0 cursor-pointer" |
| | | :style="tiltStyle('purchase')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'purchase')" |
| | | @mouseleave="onMouseLeave('purchase')" |
| | | @click="goto('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-indigo-500 to-blue-400 shadow-lg shadow-indigo-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-indigo-600 dark:text-indigo-400" |
| | | :end-val="purchaseTotal" |
| | | :duration="1200" |
| | | /> |
| | | <span class="text-xs text-gray-400">笔</span> |
| | | </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 class="mt-1 flex items-center gap-1 text-xs text-gray-400"> |
| | | <span class="inline-block size-1.5 rounded-full bg-orange-400" /> 待审批 |
| | | {{ purchasePending }} |
| | | </div> |
| | | </Col> |
| | | </Row> |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="absolute inset-0 -z-0 bg-gradient-to-br from-indigo-500/5 to-blue-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" |
| | | /> |
| | | <div |
| | | class="absolute -right-6 -top-6 size-24 rounded-full bg-indigo-400/10 blur-xl transition-all duration-300 group-hover:scale-150" |
| | | /> |
| | | </div> |
| | | </Col> |
| | | </Row> |