Kaynağa Gözat

fix: improve file picker drop functionality and error handling

Denis 10 ay önce
ebeveyn
işleme
b67f358b2c
1 değiştirilmiş dosya ile 99 ekleme ve 78 silme
  1. 99 78
      src/components/defaults/DefaultFilePicker.vue

+ 99 - 78
src/components/defaults/DefaultFilePicker.vue

@@ -4,30 +4,31 @@
     v-bind="$attrs"
     borderless
     :rules="rules"
-    :error
-    :error-message
+    :error="error"
+    :error-message="errorMessage"
+    class="custom-file-input"
   >
     <div class="column flex-center q-mb-sm full-width">
-      <span class="text-grey-6">{{ label }}</span>
+      <span v-if="label" class="text-grey-6 q-mb-xs">{{ label }}</span>
       <div
         class="image-preview-container"
         :class="{
           'has-image': preview,
           'is-dragging': isDragging,
         }"
-        @click="pickImage"
-        @dragover.prevent="handleDragOver"
-        @dragleave.prevent="handleDragLeave"
-        @drop.prevent="handleDrop"
+        @click="pickFile"
+        @dragover="handleDragOver"
+        @dragleave="handleDragLeave"
+        @drop="handleDrop"
       >
         <template v-if="!preview">
           <q-icon
             :name="
               isDragging
-                ? 'file_upload'
-                : type == 'image'
-                  ? 'add_photo_alternate'
-                  : 'insert_drive_file'
+                ? 'mdi-upload'
+                : type === 'image'
+                  ? 'mdi-image-plus'
+                  : 'mdi-file-plus'
             "
             size="48px"
             color="grey-6"
@@ -45,40 +46,47 @@
             }}
           </div>
         </template>
+
         <q-img
-          v-else-if="type == 'image'"
+          v-else-if="type === 'image'"
           :src="preview"
           fit="cover"
           class="full-height"
-        >
-          <div class="absolute-bottom text-right">
-            <q-btn
-              flat
-              dense
-              round
-              color="negative"
-              icon="delete"
-              @click.stop="clearImage"
-            />
-          </div>
-        </q-img>
+        />
+
         <div v-else class="position-relative column full-height flex-center">
           <q-icon name="mdi-file-check" size="48px" color="grey-6" />
           <div
             class="absolute-bottom text-caption text-grey-6 text-center q-mb-sm q-px-md"
           >
-            {{ preview }}
+            {{ model.name }}
           </div>
         </div>
+
+        <div v-if="preview" class="absolute-top-right q-ma-xs">
+          <q-btn
+            flat
+            dense
+            round
+            color="negative"
+            icon="mdi-close"
+            @click.stop="clearFile"
+          />
+        </div>
       </div>
 
-      <q-file v-show="false" ref="fileInput" v-model="model" :accept="accept" />
+      <q-file
+        v-show="false"
+        ref="fileInputRef"
+        v-model="model"
+        :accept="accept"
+      />
     </div>
   </q-field>
 </template>
 
 <script setup>
-import { ref, useTemplateRef, watch } from "vue";
+import { ref, watch, onUnmounted, useTemplateRef } from "vue";
 
 const { label, rules, accept, type, initialImage } = defineProps({
   label: {
@@ -111,58 +119,66 @@ const { label, rules, accept, type, initialImage } = defineProps({
   },
 });
 
-const model = defineModel();
+const model = defineModel({ type: [File, String, null], default: null });
 const base64File = defineModel("base64File", { type: String, default: null });
-const preview = ref(initialImage);
+
 const isDragging = ref(false);
-const fileInput = useTemplateRef("fileInput");
+const fileInputRef = useTemplateRef("fileInputRef");
+const preview = ref(initialImage || null);
+let objectUrl = null;
 
-const processFile = async (file) => {
-  if (!file) {
-    console.error("No file provided");
-    return;
+const cleanupObjectURL = () => {
+  if (objectUrl) {
+    URL.revokeObjectURL(objectUrl);
+    objectUrl = null;
   }
+};
+onUnmounted(cleanupObjectURL);
 
-  if (type == "image" && !file.type.startsWith("image/")) {
-    console.error("Invalid file type");
+const generateBase64 = (file) => {
+  if (!file) {
+    base64File.value = null;
     return;
   }
+  const reader = new FileReader();
+  reader.onload = (e) => {
+    base64File.value = e.target.result;
+  };
+  reader.onerror = () => {
+    console.error("FileReader failed to read file.");
+    base64File.value = null;
+  };
+  reader.readAsDataURL(file);
+};
+
+watch(model, (newFile) => {
+  cleanupObjectURL();
 
-  if (type == "file") {
-    const blob = new Blob([file], { type: file.type });
-    preview.value = file.name;
-    return new Promise((resolve) => {
-      const reader = new FileReader();
-      reader.onload = (e) => {
-        base64File.value = e.target.result;
-        console.log(preview.value);
-        resolve();
-      };
-      reader.readAsDataURL(blob);
-    });
+  if (newFile && newFile instanceof File) {
+    if (type === "image") {
+      objectUrl = URL.createObjectURL(newFile);
+      preview.value = objectUrl;
+    } else {
+      preview.value = "file_selected";
+    }
+    generateBase64(newFile);
   } else {
-    return new Promise((resolve) => {
-      const reader = new FileReader();
-      reader.onload = (e) => {
-        base64File.value = e.target.result;
-        preview.value = e.target.result;
-        resolve();
-      };
-      reader.readAsDataURL(file);
-    });
+    preview.value = initialImage || null;
+    base64File.value = null;
   }
-};
+});
 
-const pickImage = () => {
-  fileInput.value?.pickFiles();
+const pickFile = () => {
+  fileInputRef.value?.pickFiles();
 };
 
-const clearImage = () => {
+const clearFile = () => {
   model.value = null;
-  preview.value = null;
 };
 
-const handleDragOver = () => {
+const handleDragOver = (event) => {
+  event.preventDefault();
+  event.dataTransfer.dropEffect = "copy";
   isDragging.value = true;
 };
 
@@ -170,26 +186,31 @@ const handleDragLeave = () => {
   isDragging.value = false;
 };
 
-const handleDrop = async (event) => {
+const handleDrop = (event) => {
+  event.preventDefault();
   isDragging.value = false;
+
   const file = event.dataTransfer?.files?.[0];
+  if (!file) return;
 
-  if (file) {
-    model.value = file;
-    await processFile(file);
-  }
-};
+  const acceptedMime = accept;
 
-watch(
-  () => model.value,
-  async (value, oldValue) => {
-    if (value != oldValue) {
-      await processFile(value);
-    } else {
-      preview.value = null;
+  if (acceptedMime.endsWith("/*")) {
+    const baseMime = acceptedMime.replace("/*", "");
+    if (file.type.startsWith(baseMime + "/")) {
+      model.value = file;
     }
-  },
-);
+  } else {
+    if (
+      acceptedMime
+        .split(",")
+        .map((m) => m.trim())
+        .includes(file.type)
+    ) {
+      model.value = file;
+    }
+  }
+};
 </script>
 
 <style lang="scss" scoped>