<script lang="ts" setup>
|
import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
import { gsap } from 'gsap';
|
import { IconifyIcon } from '@vben/icons';
|
|
import { Card, Col, Row } from 'ant-design-vue';
|
|
defineOptions({ name: 'BagCodeHomeKpiCards' });
|
|
const props = defineProps<{
|
summary: MesBagCodeHomeApi.Summary;
|
scanTotal: number; // 累计扫码次数
|
}>();
|
|
const emit = defineEmits<{
|
navigate: [name: string];
|
}>();
|
|
/** 全链路 KPI 卡片:覆盖基础数据/生产/一袋一码/出库/流通/仓储/质量(翠绿主题,预警/不合格保留语义色) */
|
const cards = computed(() => [
|
{ gradient: 'linear-gradient(135deg, #10b981, #047857)', icon: 'lucide:package', name: '品种总数', routeName: 'MdmItem', sub: '基础数据', value: () => props.summary.mdmItemCount ?? 0 },
|
{ gradient: 'linear-gradient(135deg, #34d399, #059669)', icon: 'lucide:layers', name: '生产批次', routeName: 'MesProBatch', sub: '一袋一码', value: () => props.summary.batchCount ?? 0 },
|
{ gradient: 'linear-gradient(135deg, #059669, #047857)', icon: 'lucide:scan-barcode', name: '袋码', routeName: 'MesProBagCode', sub: '赋码', value: () => props.summary.bagCodeCount ?? 0 },
|
{ gradient: 'linear-gradient(135deg, #6ee7b7, #10b981)', icon: 'lucide:grid-2x2', name: '托盘码', routeName: 'MesProPallet', sub: '托盘', value: () => props.summary.palletCount ?? 0 },
|
{ gradient: 'linear-gradient(135deg, #0d9488, #0f766e)', icon: 'lucide:truck', name: '销售出库单', routeName: 'MesWmProductSales', sub: '出库流通', value: () => props.summary.productSalesCount ?? 0 },
|
{ gradient: 'linear-gradient(135deg, #10b981, #0d9488)', icon: 'lucide:scan-line', name: '累计扫码', routeName: 'MesConsumerScanStatistics', sub: '溯源', value: () => props.scanTotal ?? 0 },
|
{ gradient: 'linear-gradient(135deg, #f59e0b, #b45309)', icon: 'lucide:bell-alert', name: '库存预警', routeName: 'MesWmStockWarning', sub: '仓储', value: () => props.summary.stockWarningPendingCount ?? 0 },
|
{ gradient: 'linear-gradient(135deg, #f43f5e, #e11d48)', icon: 'lucide:shield-x', name: '待评审不合格', routeName: 'MesQcNcr', sub: '质量', value: () => props.summary.ncrPendingCount ?? 0 },
|
]);
|
|
/** 每个卡片目标数字;display 为 GSAP 计数动画过程中展示的文本 */
|
const targets = computed(() => cards.value.map((card) => card.value()));
|
const display = ref<string[]>(cards.value.map(() => '0'));
|
|
let tweens: gsap.core.Tween[] = [];
|
|
/** 用 GSAP 把数字从 0 滚动到目标值(千分位格式化) */
|
function animateTo(to: number[]) {
|
tweens.forEach((t) => t.kill());
|
tweens = [];
|
display.value = to.map(() => '0');
|
to.forEach((endVal, index) => {
|
const proxy = { v: 0 };
|
tweens.push(
|
gsap.to(proxy, {
|
duration: 1.2,
|
ease: 'power2.out',
|
onComplete: () => {
|
display.value[index] = endVal.toLocaleString();
|
},
|
onUpdate: () => {
|
display.value[index] = Math.round(proxy.v).toLocaleString();
|
},
|
v: endVal,
|
}),
|
);
|
});
|
}
|
|
onMounted(() => {
|
animateTo(targets.value);
|
});
|
|
// 汇总数据加载后,数字由 0 滚动到真实值
|
watch(targets, (val) => animateTo(val));
|
|
onUnmounted(() => {
|
tweens.forEach((t) => t.kill());
|
tweens = [];
|
});
|
</script>
|
|
<template>
|
<Row :gutter="16">
|
<Col
|
v-for="(card, index) in cards"
|
:key="card.name"
|
:lg="6"
|
:md="12"
|
:sm="12"
|
:xl="6"
|
:xs="24"
|
class="mb-4"
|
>
|
<Card
|
class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"
|
@click="emit('navigate', card.routeName)"
|
>
|
<div class="flex items-center gap-4">
|
<div
|
class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
|
:style="{ background: card.gradient }"
|
>
|
<IconifyIcon class="size-7" :icon="card.icon" />
|
</div>
|
<div class="min-w-0">
|
<div class="text-muted-foreground mb-1 text-sm">{{ card.name }}</div>
|
<div class="text-2xl font-bold leading-tight">{{ display[index] }}</div>
|
<div class="text-muted-foreground mt-1 truncate text-xs">{{ card.sub }}</div>
|
</div>
|
</div>
|
</Card>
|
</Col>
|
</Row>
|
</template>
|