<script lang="ts" setup>
|
import type { MesHomeApi } from '#/api/mes/home';
|
|
import { computed, ref } from 'vue';
|
|
import { CountTo } from '@vben/common-ui';
|
import { IconifyIcon } from '@vben/icons';
|
|
import { Col, Row } from 'ant-design-vue';
|
|
defineOptions({ name: 'DashboardKpiCards' });
|
|
const props = defineProps<{
|
summary: MesHomeApi.Summary;
|
}>();
|
|
const emit = defineEmits<{
|
navigate: [name: string];
|
}>();
|
|
const qualityRate = computed(() => {
|
const total =
|
props.summary.todayQualifiedQuantity +
|
props.summary.todayUnqualifiedQuantity;
|
return total === 0 ? 0 : (props.summary.todayQualifiedQuantity / total) * 100;
|
});
|
|
const equipmentRate = computed(() => {
|
if (props.summary.machineryTotal === 0) return 0;
|
return (props.summary.machineryProducing / props.summary.machineryTotal) * 100;
|
});
|
|
/** 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',
|
};
|
}
|
</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('order')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'order')"
|
@mouseleave="onMouseLeave('order')"
|
@click="emit('navigate', 'MesProWorkOrder')"
|
>
|
<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-blue-500 to-cyan-400 shadow-lg shadow-blue-500/25 transition-transform duration-300 group-hover:scale-110">
|
<IconifyIcon class="size-6 text-white" icon="lucide:clipboard-list" />
|
</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-blue-600 dark:text-blue-400" :end-val="summary.workOrderActiveCount" :duration="1200" />
|
<span 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-amber-400" /> 待排产 {{ summary.workOrderPrepareCount }}
|
<span class="mx-1">|</span>
|
<span class="inline-block size-1.5 rounded-full bg-emerald-400" /> 已完成 {{ summary.workOrderFinishedCount }}
|
</div>
|
</div>
|
</div>
|
<div class="absolute inset-0 -z-0 bg-gradient-to-br from-blue-500/5 to-cyan-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
<div class="absolute -right-6 -top-6 size-24 rounded-full bg-blue-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('output')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'output')"
|
@mouseleave="onMouseLeave('output')"
|
@click="emit('navigate', 'MesProFeedback')"
|
>
|
<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-emerald-500 to-green-400 shadow-lg shadow-emerald-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-emerald-600 dark:text-emerald-400" :end-val="summary.todayOutput" :duration="1200" />
|
<span class="text-xs text-gray-400">件</span>
|
</div>
|
<div class="mt-1 text-xs text-gray-400">
|
昨日 {{ summary.yesterdayOutput }} 件
|
<span v-if="summary.yesterdayOutput > 0 && summary.todayOutput >= summary.yesterdayOutput" class="text-emerald-500">↑</span>
|
<span v-else-if="summary.yesterdayOutput > 0" class="text-red-500">↓</span>
|
</div>
|
</div>
|
</div>
|
<div class="absolute inset-0 -z-0 bg-gradient-to-br from-emerald-500/5 to-green-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
<div class="absolute -right-6 -top-6 size-24 rounded-full bg-emerald-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('quality')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')"
|
@mouseleave="onMouseLeave('quality')"
|
@click="emit('navigate', 'MesQcRecord')"
|
>
|
<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-orange-500 to-amber-400 shadow-lg shadow-orange-500/25 transition-transform duration-300 group-hover:scale-110">
|
<IconifyIcon class="size-6 text-white" icon="lucide:shield-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"
|
:class="qualityRate >= 95 ? 'text-emerald-600 dark:text-emerald-400' : qualityRate >= 80 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400'"
|
:decimals="1" :end-val="qualityRate" :duration="1200"
|
/>
|
<span class="text-xs text-gray-400">%</span>
|
</div>
|
<div class="mt-1 text-xs text-gray-400">
|
合格 {{ summary.todayQualifiedQuantity }} | 不良 {{ summary.todayUnqualifiedQuantity }}
|
</div>
|
</div>
|
</div>
|
<div class="absolute inset-0 -z-0 bg-gradient-to-br from-orange-500/5 to-amber-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
<div class="absolute -right-6 -top-6 size-24 rounded-full bg-orange-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('equip')"
|
@mousemove="(e: MouseEvent) => onMouseMove(e, 'equip')"
|
@mouseleave="onMouseLeave('equip')"
|
@click="emit('navigate', 'MesDvMachinery')"
|
>
|
<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:cpu" />
|
</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-violet-600 dark:text-violet-400" :decimals="1" :end-val="equipmentRate" :duration="1200" />
|
<span class="text-xs text-gray-400">%</span>
|
</div>
|
<div class="mt-1 flex items-center gap-1 text-xs text-gray-400">
|
<span class="text-red-400">停机 {{ summary.machineryStop }}</span>
|
<span class="mx-1">|</span>
|
<span class="text-amber-400">维护 {{ summary.machineryMaintenance }}</span>
|
</div>
|
</div>
|
</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>
|
</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>
|