OrderTracker.vue 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. <template>
  2. <div class="w-full py-4">
  3. <div class="flex items-center justify-between relative">
  4. <!-- Background line -->
  5. <div class="absolute left-0 top-1/2 -translate-y-1/2 w-full h-1 bg-border/50 rounded-full z-0"></div>
  6. <!-- Colored line up to current step -->
  7. <div
  8. class="absolute left-0 top-1/2 -translate-y-1/2 h-1 bg-primary rounded-full z-0 transition-all duration-1000 ease-in-out"
  9. :style="{ width: progressWidth }"
  10. ></div>
  11. <!-- Steps -->
  12. <div
  13. v-for="(step, i) in steps"
  14. :key="step.id"
  15. class="relative z-10 flex flex-col items-center gap-2"
  16. :class="[i <= currentIndex ? 'opacity-100' : 'opacity-40 grayscale']"
  17. >
  18. <div
  19. class="w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center transition-all duration-500 shadow-sm"
  20. :class="[
  21. i < currentIndex ? 'bg-primary text-primary-foreground' :
  22. i === currentIndex ? 'bg-primary text-primary-foreground ring-4 ring-primary/20 animate-pulse' :
  23. 'bg-card border-2 border-border text-muted-foreground'
  24. ]"
  25. >
  26. <component :is="step.icon" class="w-4 h-4 sm:w-5 sm:h-5" />
  27. </div>
  28. <span class="text-[10px] sm:text-xs font-bold whitespace-nowrap hidden sm:block" :class="i <= currentIndex ? 'text-foreground' : 'text-muted-foreground'">
  29. {{ step.label }}
  30. </span>
  31. </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script setup lang="ts">
  36. import { computed } from 'vue';
  37. import { Clock, ShieldCheck, Printer, Truck, PackageCheck, XCircle } from 'lucide-vue-next';
  38. import { useI18n } from 'vue-i18n';
  39. const props = defineProps<{
  40. status: string;
  41. }>();
  42. const { t } = useI18n();
  43. const steps = computed(() => {
  44. if (props.status === 'cancelled') {
  45. return [
  46. { id: 'pending', icon: Clock, label: t('statuses.pending') },
  47. { id: 'cancelled', icon: XCircle, label: t('statuses.cancelled') }
  48. ];
  49. }
  50. return [
  51. { id: 'pending', icon: Clock, label: t('statuses.pending') },
  52. { id: 'processing', icon: ShieldCheck, label: t('statuses.approved') },
  53. { id: 'printing', icon: Printer, label: t('statuses.printing') },
  54. { id: 'shipped', icon: Truck, label: t('statuses.shipped') },
  55. { id: 'completed', icon: PackageCheck, label: t('statuses.delivered') }
  56. ];
  57. });
  58. const currentIndex = computed(() => {
  59. // If printing doesn't exist in actual DB, we map processing->processing
  60. // but if the status is "printing", we'd use it.
  61. // Standard statuses: pending, processing, shipped, completed
  62. const map: Record<string, number> = {
  63. 'pending': 0,
  64. 'processing': 1,
  65. 'printing': 2,
  66. 'shipped': 3,
  67. 'completed': 4,
  68. 'cancelled': 1
  69. };
  70. return map[props.status] ?? 0;
  71. });
  72. const progressWidth = computed(() => {
  73. if (steps.value.length === 0) return '0%';
  74. return `${(currentIndex.value / (steps.value.length - 1)) * 100}%`;
  75. });
  76. </script>