vue · 3906 bytes Raw Blame History
1 <template>
2 <div class="upload-progress bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
3 <div class="flex items-center justify-between mb-2">
4 <div class="flex items-center space-x-3 flex-1 min-w-0">
5 <!-- Status icon -->
6 <div class="flex-shrink-0">
7 <div
8 v-if="upload.status === 'uploading'"
9 class="spinner w-4 h-4"
10 ></div>
11 <CheckCircleIcon
12 v-else-if="upload.status === 'completed'"
13 class="w-5 h-5 text-green-500"
14 />
15 <ExclamationCircleIcon
16 v-else-if="upload.status === 'error'"
17 class="w-5 h-5 text-red-500"
18 />
19 </div>
20
21 <!-- File info -->
22 <div class="flex-1 min-w-0">
23 <p class="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
24 {{ upload.filename }}
25 </p>
26 <p class="text-xs text-gray-500 dark:text-gray-400">
27 {{ getStatusText() }}
28 </p>
29 </div>
30 </div>
31
32 <!-- Actions -->
33 <div class="flex items-center space-x-2">
34 <!-- Progress percentage -->
35 <span
36 v-if="upload.status === 'uploading'"
37 class="text-sm text-gray-600 dark:text-gray-400 tabular-nums"
38 >
39 {{ Math.round(upload.progress) }}%
40 </span>
41
42 <!-- Cancel/Remove button -->
43 <button
44 @click="handleAction"
45 class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 rounded"
46 :title="upload.status === 'uploading' ? 'Cancel upload' : 'Remove'"
47 >
48 <XMarkIcon class="w-4 h-4" />
49 </button>
50 </div>
51 </div>
52
53 <!-- Progress bar -->
54 <div
55 v-if="upload.status === 'uploading'"
56 class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden"
57 >
58 <div
59 class="progress-bar h-full bg-primary-600 transition-all duration-300 ease-out"
60 :style="{ width: `${upload.progress}%` }"
61 ></div>
62 </div>
63
64 <!-- Error message -->
65 <div
66 v-if="upload.status === 'error' && upload.error"
67 class="mt-2 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md"
68 >
69 <p class="text-sm text-red-600 dark:text-red-400">
70 {{ upload.error }}
71 </p>
72 </div>
73
74 <!-- Completed message -->
75 <div
76 v-if="upload.status === 'completed'"
77 class="mt-2 p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md"
78 >
79 <p class="text-sm text-green-600 dark:text-green-400">
80 Upload completed successfully
81 </p>
82 </div>
83 </div>
84 </template>
85
86 <script setup lang="ts">
87 import { CheckCircleIcon, ExclamationCircleIcon, XMarkIcon } from '@heroicons/vue/24/outline'
88
89 interface UploadProgress {
90 fileId: string
91 filename: string
92 progress: number
93 status: 'uploading' | 'completed' | 'error'
94 error?: string
95 }
96
97 // Props
98 const props = defineProps<{
99 upload: UploadProgress
100 }>()
101
102 // Emits
103 const emit = defineEmits<{
104 cancel: [fileId: string]
105 remove: [fileId: string]
106 }>()
107
108 function getStatusText(): string {
109 switch (props.upload.status) {
110 case 'uploading':
111 return 'Uploading...'
112 case 'completed':
113 return 'Upload completed'
114 case 'error':
115 return 'Upload failed'
116 default:
117 return ''
118 }
119 }
120
121 function handleAction() {
122 if (props.upload.status === 'uploading') {
123 emit('cancel', props.upload.fileId)
124 } else {
125 emit('remove', props.upload.fileId)
126 }
127 }
128 </script>
129
130 <style scoped>
131 .progress-bar {
132 background: linear-gradient(90deg, #3b82f6, #1d4ed8);
133 }
134
135 .spinner {
136 border: 2px solid #e5e7eb;
137 border-top: 2px solid #3b82f6;
138 border-radius: 50%;
139 animation: spin 1s linear infinite;
140 }
141
142 @keyframes spin {
143 0% { transform: rotate(0deg); }
144 100% { transform: rotate(360deg); }
145 }
146 </style>