<script lang="ts" setup>
|
import { onMounted, ref } from 'vue';
|
|
import { CountTo } from '@vben/common-ui';
|
import { IconifyIcon } from '@vben/icons';
|
|
import { Col, Row } from 'ant-design-vue';
|
|
import { getPurchaseOrderPage } from '#/api/erp/purchase/order';
|
import { getSaleOrderPage } from '#/api/erp/sale/order';
|
import { getMaterialStockPage } from '#/api/mes/wm/materialstock';
|
import { getTaskTodoPage } from '#/api/bpm/task';
|
|
defineOptions({ name: 'DashboardBizCards' });
|
|
const emit = defineEmits<{
|
navigate: [name: string];
|
}>();
|
|
const purchaseCount = ref(0);
|
const saleCount = ref(0);
|
const stockCount = ref(0);
|
const bpmTodoCount = ref(0);
|
|
async function loadPurchase() {
|
try {
|
const result = await getPurchaseOrderPage({ pageNo: 1, pageSize: 1 });
|
purchaseCount.value = result.total ?? 0;
|
} catch { /* 无权限或接口异常 */ }
|
}
|
|
async function loadSale() {
|
try {
|
const result = await getSaleOrderPage({ pageNo: 1, pageSize: 1 });
|
saleCount.value = result.total ?? 0;
|
} catch { /* 无权限或接口异常 */ }
|
}
|
|
async function loadStock() {
|
try {
|
const result = await getMaterialStockPage({ pageNo: 1, pageSize: 1 });
|
stockCount.value = result.total ?? 0;
|
} catch { /* 无权限或接口异常 */ }
|
}
|
|
async function loadBpmTodo() {
|
try {
|
const result = await getTaskTodoPage({ pageNo: 1, pageSize: 1 });
|
bpmTodoCount.value = result.total ?? 0;
|
} catch { /* 无权限或接口异常 */ }
|
}
|
|
/** 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 * -5, y: y * 5 };
|
}
|
|
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(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(() => {
|
loadPurchase();
|
loadSale();
|
loadStock();
|
loadBpmTodo();
|
});
|
</script>
|
|
<template>
|
<Row :gutter="16">
|
<!-- 采购订单 -->
|
<Col :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('purchase')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'purchase')"
|
@mouseleave="onMouseLeave('purchase')"
|
@click="emit('navigate', '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-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: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-cyan-600 dark:text-cyan-400" :end-val="purchaseCount" :duration="1200" />
|
<span class="text-xs text-gray-400">笔</span>
|
</div>
|
<div class="mt-1 text-xs text-gray-400">ERP 采购管理</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 :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('sale')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'sale')"
|
@mouseleave="onMouseLeave('sale')"
|
@click="emit('navigate', 'ErpSaleOrder')"
|
>
|
<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-pink-500 to-rose-400 shadow-lg shadow-pink-500/25 transition-transform duration-300 group-hover:scale-110">
|
<IconifyIcon class="size-6 text-white" icon="lucide:trending-up" />
|
</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-pink-600 dark:text-pink-400" :end-val="saleCount" :duration="1200" />
|
<span class="text-xs text-gray-400">笔</span>
|
</div>
|
<div class="mt-1 text-xs text-gray-400">ERP 销售管理</div>
|
</div>
|
</div>
|
<div class="absolute inset-0 -z-0 bg-gradient-to-br from-pink-500/5 to-rose-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
<div class="absolute -right-6 -top-6 size-24 rounded-full bg-pink-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
|
</div>
|
</Col>
|
|
<!-- 物料库存 -->
|
<Col :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('stock')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'stock')"
|
@mouseleave="onMouseLeave('stock')"
|
@click="emit('navigate', '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-amber-500 to-yellow-400 shadow-lg shadow-amber-500/25 transition-transform duration-300 group-hover:scale-110">
|
<IconifyIcon class="size-6 text-white" icon="lucide:package-search" />
|
</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="stockCount" :duration="1200" />
|
<span class="text-xs text-gray-400">条</span>
|
</div>
|
<div class="mt-1 text-xs text-gray-400">MES 库存台账</div>
|
</div>
|
</div>
|
<div class="absolute inset-0 -z-0 bg-gradient-to-br from-amber-500/5 to-yellow-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>
|
|
<!-- BPM 待办 -->
|
<Col :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('bpm')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'bpm')"
|
@mouseleave="onMouseLeave('bpm')"
|
@click="emit('navigate', 'BpmTaskTodo')"
|
>
|
<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-red-500 to-rose-400 shadow-lg shadow-red-500/25 transition-transform duration-300 group-hover:scale-110">
|
<IconifyIcon class="size-6 text-white" icon="lucide:clipboard-check" />
|
</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-red-600 dark:text-red-400" :end-val="bpmTodoCount" :duration="1200" />
|
<span class="text-xs text-gray-400">项</span>
|
</div>
|
<div class="mt-1 text-xs text-gray-400">BPM 待审批</div>
|
</div>
|
</div>
|
<div class="absolute inset-0 -z-0 bg-gradient-to-br from-red-500/5 to-rose-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
<div class="absolute -right-6 -top-6 size-24 rounded-full bg-red-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
|
</div>
|
</Col>
|
</Row>
|
</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 1px 4px rgba(0,0,0,0.2),
|
inset 0 1px 0 rgba(255,255,255,0.04);
|
}
|
</style>
|