8 天以前 fdd158306bf6dc3584f5110a385323f4a8dfb463
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<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>