From 8a1e3bc7ea351d4fceac4185c5fe7f7f32a76974 Mon Sep 17 00:00:00 2001
From: xiaoyi <908147524@qq.com>
Date: 星期五, 30 十月 2026 20:21:08 +0800
Subject: [PATCH] style(dashboard): 出勤指标与库存采购改为标准 KPI 卡片,与首页统一
---
src/views/dashboard/analytics/modules/hr-stock-cards.vue | 367 +++++++++++++++++++++++++++++++--------------------
src/views/dashboard/analytics/index.vue | 2
2 files changed, 224 insertions(+), 145 deletions(-)
diff --git a/src/views/dashboard/analytics/index.vue b/src/views/dashboard/analytics/index.vue
index a0792a0..14c87a5 100644
--- a/src/views/dashboard/analytics/index.vue
+++ b/src/views/dashboard/analytics/index.vue
@@ -84,7 +84,7 @@
<BizCards @navigate="handleNavigate" />
<!-- 鍑哄嫟鎸囨爣 + 搴撳瓨閲囪喘 -->
- <HrStockCards class="mb-4 block" @navigate="handleNavigate" />
+ <HrStockCards @navigate="handleNavigate" />
<!-- AI 鏅鸿兘鍒嗘瀽 -->
<AiInsights
diff --git a/src/views/dashboard/analytics/modules/hr-stock-cards.vue b/src/views/dashboard/analytics/modules/hr-stock-cards.vue
index 36ccdbb..c8a12cf 100644
--- a/src/views/dashboard/analytics/modules/hr-stock-cards.vue
+++ b/src/views/dashboard/analytics/modules/hr-stock-cards.vue
@@ -4,7 +4,7 @@
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';
@@ -17,15 +17,19 @@
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);
@@ -36,66 +40,107 @@
}
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();
@@ -105,138 +150,172 @@
</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">鎵i櫎鍐荤粨涓庡崰鐢�</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> 鎵i櫎鍐荤粨鍗犵敤
</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>
--
Gitblit v1.9.3